<!DOCTYPE html>
<html lang="zh-CN">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=2">
  <meta name="theme-color" content="#222">
  <meta name="generator" content="Hexo 4.2.1">
  <link rel="apple-touch-icon" sizes="180x180" href="/images/apple-touch-icon-next.png">
  <link rel="icon" type="image/png" sizes="32x32" href="/images/favicon-32x32-next.png">
  <link rel="icon" type="image/png" sizes="16x16" href="/images/favicon-16x16-next.png">
  <link rel="mask-icon" href="/images/safari-pinned-tab.svg" color="#222">
  <link rel="stylesheet" href="/css/main.css">
  <link rel="stylesheet" href="/lib/font-awesome/css/all.min.css">
  <link rel="stylesheet" href="/lib/pace/pace-theme-minimal.min.css">
  <script src="/lib/pace/pace.min.js"></script>
  <script id="hexo-configurations">
    var NexT = window.NexT ||
    {};
    var CONFIG = {
      "hostname": "cuiqingcai.com",
      "root": "/",
      "scheme": "Pisces",
      "version": "7.8.0",
      "exturl": false,
      "sidebar":
      {
        "position": "right",
        "width": 360,
        "display": "post",
        "padding": 18,
        "offset": 12,
        "onmobile": false,
        "widgets": [
          {
            "type": "image",
            "name": "阿布云",
            "enable": false,
            "url": "https://www.abuyun.com/http-proxy/introduce.html",
            "src": "https://qiniu.cuiqingcai.com/88au8.jpg",
            "width": "100%"
      },
          {
            "type": "image",
            "name": "天验",
            "enable": true,
            "url": "https://tutorial.lengyue.video/?coupon=12ef4b1a-a3db-11ea-bb37-0242ac130002_cqx_850",
            "src": "https://qiniu.cuiqingcai.com/bco2a.png",
            "width": "100%"
      },
          {
            "type": "image",
            "name": "华为云",
            "enable": false,
            "url": "https://activity.huaweicloud.com/2020_618_promotion/index.html?bpName=5f9f98a29e2c40b780c1793086f29fe2&bindType=1&salesID=wangyubei",
            "src": "https://qiniu.cuiqingcai.com/y42ik.jpg",
            "width": "100%"
      },
          {
            "type": "image",
            "name": "张小鸡",
            "enable": false,
            "url": "http://www.zxiaoji.com/",
            "src": "https://qiniu.cuiqingcai.com/fm72f.png",
            "width": "100%"
      },
          {
            "type": "image",
            "name": "Luminati",
            "src": "https://qiniu.cuiqingcai.com/ikkq9.jpg",
            "url": "https://luminati-china.io/?affiliate=ref_5fbbaaa9647883f5c6f77095",
            "width": "100%",
            "enable": false
      },
          {
            "type": "image",
            "name": "IPIDEA",
            "url": "http://www.ipidea.net/?utm-source=cqc&utm-keyword=?cqc",
            "src": "https://qiniu.cuiqingcai.com/0ywun.png",
            "width": "100%",
            "enable": true
      },
          {
            "type": "tags",
            "name": "标签云",
            "enable": true
      },
          {
            "type": "categories",
            "name": "分类",
            "enable": true
      },
          {
            "type": "friends",
            "name": "友情链接",
            "enable": true
      },
          {
            "type": "hot",
            "name": "猜你喜欢",
            "enable": true
      }]
      },
      "copycode":
      {
        "enable": true,
        "show_result": true,
        "style": "mac"
      },
      "back2top":
      {
        "enable": true,
        "sidebar": false,
        "scrollpercent": true
      },
      "bookmark":
      {
        "enable": false,
        "color": "#222",
        "save": "auto"
      },
      "fancybox": false,
      "mediumzoom": false,
      "lazyload": false,
      "pangu": true,
      "comments":
      {
        "style": "tabs",
        "active": "gitalk",
        "storage": true,
        "lazyload": false,
        "nav": null,
        "activeClass": "gitalk"
      },
      "algolia":
      {
        "hits":
        {
          "per_page": 10
        },
        "labels":
        {
          "input_placeholder": "Search for Posts",
          "hits_empty": "We didn't find any results for the search: ${query}",
          "hits_stats": "${hits} results found in ${time} ms"
        }
      },
      "localsearch":
      {
        "enable": true,
        "trigger": "auto",
        "top_n_per_article": 10,
        "unescape": false,
        "preload": false
      },
      "motion":
      {
        "enable": false,
        "async": false,
        "transition":
        {
          "post_block": "bounceDownIn",
          "post_header": "slideDownIn",
          "post_body": "slideDownIn",
          "coll_header": "slideLeftIn",
          "sidebar": "slideUpIn"
        }
      },
      "path": "search.xml"
    };

  </script>
  <meta name="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
  <meta property="og:type" content="website">
  <meta property="og:title" content="静觅">
  <meta property="og:url" content="https://cuiqingcai.com/page/9/index.html">
  <meta property="og:site_name" content="静觅">
  <meta property="og:description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
  <meta property="og:locale" content="zh_CN">
  <meta property="article:author" content="崔庆才">
  <meta property="article:tag" content="崔庆才">
  <meta property="article:tag" content="静觅">
  <meta property="article:tag" content="PHP">
  <meta property="article:tag" content="Java">
  <meta property="article:tag" content="Python">
  <meta property="article:tag" content="Spider">
  <meta property="article:tag" content="爬虫">
  <meta property="article:tag" content="Web">
  <meta property="article:tag" content="Kubernetes">
  <meta property="article:tag" content="深度学习">
  <meta property="article:tag" content="机器学习">
  <meta property="article:tag" content="数据分析">
  <meta property="article:tag" content="网络">
  <meta property="article:tag" content="IT">
  <meta property="article:tag" content="技术">
  <meta property="article:tag" content="博客">
  <meta name="twitter:card" content="summary">
  <link rel="canonical" href="https://cuiqingcai.com/page/9/">
  <script id="page-configurations">
    // https://hexo.io/docs/variables.html
    CONFIG.page = {
      sidebar: "",
      isHome: true,
      isPost: false,
      lang: 'zh-CN'
    };

  </script>
  <title>静觅丨崔庆才的个人站点</title>
  <meta name="google-site-verification" content="p_bIcnvirkFzG2dYKuNDivKD8-STet5W7D-01woA2fc" />
  <noscript>
    <style>
      .use-motion .brand,
      .use-motion .menu-item,
      .sidebar-inner,
      .use-motion .post-block,
      .use-motion .pagination,
      .use-motion .comments,
      .use-motion .post-header,
      .use-motion .post-body,
      .use-motion .collection-header
      {
        opacity: initial;
      }

      .use-motion .site-title,
      .use-motion .site-subtitle
      {
        opacity: initial;
        top: initial;
      }

      .use-motion .logo-line-before i
      {
        left: initial;
      }

      .use-motion .logo-line-after i
      {
        right: initial;
      }

    </style>
  </noscript>
  <link rel="alternate" href="/atom.xml" title="静觅" type="application/atom+xml">
</head>

<body itemscope itemtype="http://schema.org/WebPage">
  <div class="container">
    <div class="headband"></div>
    <header class="header" itemscope itemtype="http://schema.org/WPHeader">
      <div class="header-inner">
        <div class="site-brand-container">
          <div class="site-nav-toggle">
            <div class="toggle" aria-label="切换导航栏">
              <span class="toggle-line toggle-line-first"></span>
              <span class="toggle-line toggle-line-middle"></span>
              <span class="toggle-line toggle-line-last"></span>
            </div>
          </div>
          <div class="site-meta">
            <a href="/" class="brand" rel="start">
              <span class="logo-line-before"><i></i></span>
              <h1 class="site-title">静觅 <span class="site-subtitle"> 崔庆才的个人站点 </span>
              </h1>
              <span class="logo-line-after"><i></i></span>
            </a>
          </div>
          <div class="site-nav-right">
            <div class="toggle popup-trigger">
              <i class="fa fa-search fa-fw fa-lg"></i>
            </div>
          </div>
        </div>
        <nav class="site-nav">
          <ul id="menu" class="main-menu menu">
            <li class="menu-item menu-item-home">
              <a href="/" rel="section">首页</a>
            </li>
            <li class="menu-item menu-item-archives">
              <a href="/archives/" rel="section">文章列表</a>
            </li>
            <li class="menu-item menu-item-tags">
              <a href="/tags/" rel="section">文章标签</a>
            </li>
            <li class="menu-item menu-item-categories">
              <a href="/categories/" rel="section">文章分类</a>
            </li>
            <li class="menu-item menu-item-about">
              <a href="/about/" rel="section">关于博主</a>
            </li>
            <li class="menu-item menu-item-message">
              <a href="/message/" rel="section">给我留言</a>
            </li>
            <li class="menu-item menu-item-search">
              <a role="button" class="popup-trigger">搜索 </a>
            </li>
          </ul>
        </nav>
        <div class="search-pop-overlay">
          <div class="popup search-popup">
            <div class="search-header">
              <span class="search-icon">
                <i class="fa fa-search"></i>
              </span>
              <div class="search-input-container">
                <input autocomplete="off" autocapitalize="off" placeholder="搜索..." spellcheck="false" type="search" class="search-input">
              </div>
              <span class="popup-btn-close">
                <i class="fa fa-times-circle"></i>
              </span>
            </div>
            <div id="search-result">
              <div id="no-result">
                <i class="fa fa-spinner fa-pulse fa-5x fa-fw"></i>
              </div>
            </div>
          </div>
        </div>
      </div>
    </header>
    <div class="back-to-top">
      <i class="fa fa-arrow-up"></i>
      <span>0%</span>
    </div>
    <div class="reading-progress-bar"></div>
    <main class="main">
      <div class="main-inner">
        <div class="content-wrap">
          <div class="content index posts-expand">
            <div class="carousel">
              <div id="wowslider-container">
                <div class="ws_images">
                  <ul>
                    <li><a target="_blank" href="https://cuiqingcai.com/5052.html"><img title="Python3网络爬虫开发实战教程" src="https://qiniu.cuiqingcai.com/ipy96.jpg" /></a></li>
                    <li><a target="_blank" href="https://t.lagou.com/fRCBRsRCSN6FA"><img title="52讲轻松搞定网络爬虫" src="https://qiniu.cuiqingcai.com/fqq5e.png" /></a></li>
                    <li><a target="_blank" href="https://brightdata.grsm.io/cuiqingcai"><img title="亮网络解锁器" src="https://qiniu.cuiqingcai.com/6qnb7.png" /></a></li>
                    <li><a target="_blank" href="https://cuiqingcai.com/4320.html"><img title="Python3网络爬虫开发视频教程" src="https://qiniu.cuiqingcai.com/bjrny.jpg" /></a></li>
                    <li><a target="_blank" href="https://cuiqingcai.com/5094.html"><img title="爬虫代理哪家强？十大付费代理详细对比评测出炉！" src="https://qiniu.cuiqingcai.com/nifs6.jpg" /></a></li>
                  </ul>
                </div>
                <div class="ws_thumbs">
                  <div>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/ipy96.jpg" /></a>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/fqq5e.png" /></a>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/6qnb7.png" /></a>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/bjrny.jpg" /></a>
                    <a target="_blank" href="#"><img src="https://qiniu.cuiqingcai.com/nifs6.jpg" /></a>
                  </div>
                </div>
                <div class="ws_shadow"></div>
              </div>
            </div>
            <link rel="stylesheet" href="/lib/wowslide/slide.css">
            <script src="/lib/wowslide/jquery.min.js"></script>
            <script src="/lib/wowslide/slider.js"></script>
            <script>
              jQuery("#wowslider-container").wowSlider(
              {
                effect: "cube",
                prev: "",
                next: "",
                duration: 20 * 100,
                delay: 20 * 100,
                width: 716,
                height: 297,
                autoPlay: true,
                playPause: true,
                stopOnHover: false,
                loop: false,
                bullets: 0,
                caption: true,
                captionEffect: "slide",
                controls: true,
                onBeforeStep: 0,
                images: 0
              });

            </script>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8947.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/8947.html" class="post-title-link" itemprop="url">Python 使用 environs 库来更好地定义环境变量</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>在运行一个项目的时候，我们经常会遇到设置不同环境的需求，如设置是开发环境、测试环境还是生产环境，或者在某些设置里面可能还需要设置一些变量开关，如设置调试开关、日志开关、功能开关等等。 这些变量其实就是在项目运行时我们给项目设置的一些参数。这些参数一般情况来说，可以有两种设置方法，一种是通过命令行参数，一种是通过环境变量。二者的适用范围不同，在不同的场景下我们可以选用更方便的方式来实现参数的设置。 本节我们以 Python 项目为例，说说环境变量的设置。</p>
                  <h2 id="设置和获取环境变量"><a href="#设置和获取环境变量" class="headerlink" title="设置和获取环境变量"></a>设置和获取环境变量</h2>
                  <p>首先，我们先来了解一下在 Python 项目里面怎样设置和获取变量。 首先让我们定义一个最简单的 Python 文件，命名为 main.py，内容如下：</p>
                  <figure class="highlight moonscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> <span class="built_in">os</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">os</span>.environ[<span class="string">'VAR1'</span>])</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里我们导入了 os 模块，它的 environ 对象里面就包含了当前运行状态下的所有环境变量，它其实是一个 <code>os._Environ</code> 对象，我们可以通过类似字典取值的方式从中获取里面包含的环境变量的值，如代码所示。 好，接下来我们什么也不设置，直接运行，看下结果：</p>
                  <figure class="highlight vim">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">python3</span> main.<span class="keyword">py</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>结果如下：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">raise</span> KeyError(key) <span class="keyword">from</span> <span class="keyword">None</span></span><br><span class="line">KeyError: <span class="string">'VAR1'</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>直接抛出来了一个错误，这很正常，我们此时并没有设置 VAR1 这个环境变量，当然会抛出键值异常的错误了。 接下来我们在命令行下进行设置，运行如下命令：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">VAR1</span>=germey python3 main.py</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">germey</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以看到我们在运行之前，在命令行之前通过键值对的形式对环境变量进行设置，程序就可以获取到 VAR1 这个值了，成功打印出来了 germey。 但这个环境变量是永久的吗？我们这次再运行一遍原来的命令：</p>
                  <figure class="highlight vim">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">python3</span> main.<span class="keyword">py</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>结果如下：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">raise</span> KeyError(key) <span class="keyword">from</span> <span class="keyword">None</span></span><br><span class="line">KeyError: <span class="string">'VAR1'</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>嗯，又抛错了。 这说明了什么，在命令行的前面加上的这个环境变量声明只能对当前执行的命令生效。 好，那既然如此，我难道每次运行都要在命令行前面加上这些声明吗？那岂不麻烦死了。 当然有解决方法，我们使用 export 就可以了。 比如这里，我们执行如下命令：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR1</span>=germey</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>执行完这个命令之后，当前运行环境下 VAR1 就被设置成功了，下面我们运行的命令都能获取到 VAR1 这个环境变量了。 下面来试试，还是执行原来的命令：</p>
                  <figure class="highlight vim">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">python3</span> main.<span class="keyword">py</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>结果如下：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">germey</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以，成功获取到了 VAR1 这个变量，后面我们运行的每一个命令就都会生效了。 但等一下，这个用了 export 就是永久生效了吗？ 其实并不是，其实这个 export 只对当前的命令行运行环境生效，我们只要把命令行关掉再重新打开，之前用 export 设置的环境变量就都没有了。 可以试试，重新打开命令行，再次执行原来的命令，就会又抛出键值异常的错误了。 那又有同学会问了，我要在每次命令行运行时都想自动设置好环境变量怎么办呢？ 这个就更好办了，只需要把 export 的这些命令加入到 <code>~/.bashrc</code> 文件里面就好了，每次打开命令行的时候，系统都会自动先执行以下这个脚本里面的命令，这样环境变量就设置成功了。当然这里面还有很多不同的文件，如 <code>~/.bash_profile</code> 、<code>~/.zshrc</code> 、<code>~/.profile</code>、<code>/etc/profile</code> 等等，其加载是有先后顺序的，大家感兴趣可以去了解下。 好了，扯远了，我们现在已经了解了如何设置环境变量和基本的环境变量获取方法了。</p>
                  <h2 id="更安全的获取方式"><a href="#更安全的获取方式" class="headerlink" title="更安全的获取方式"></a>更安全的获取方式</h2>
                  <p>但是上面的这种获取变量的方式实际上是非常不友好的，万一这个环境变量没设置好，那岂不是就报错了，这是很不安全的。 所以，下面再介绍几种比较友好的获取环境变量的方式，即使没有设置过，也不会报错。 我们可以把中括号取值的方式改成 get 方法，如下所示：</p>
                  <figure class="highlight moonscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> <span class="built_in">os</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">os</span>.environ.get(<span class="string">'VAR1'</span>))</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样就不会报错了，如果 VAR1 没设置，会直接返回 None，而不是直接报错。 另外我们也可以给 get 方法传入第二个参数，表示默认值，如下所示：</p>
                  <figure class="highlight moonscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> <span class="built_in">os</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">os</span>.environ.get(<span class="string">'VAR1'</span>, <span class="string">'germey'</span>))</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样即使我们如果设置过 VAR1，他就会用 germey 这个字符串代替，这就完成了默认环境变量的设置。 下面还有几种获取环境变量的方式，总结如下：</p>
                  <figure class="highlight lua">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import <span class="built_in">os</span></span><br><span class="line"><span class="built_in">print</span>(<span class="built_in">os</span>.<span class="built_in">getenv</span>(<span class="string">'VAR1'</span>, <span class="string">'germey'</span>))</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这个方式比上面的写法更简单，功能完全一致。</p>
                  <h2 id="弊端"><a href="#弊端" class="headerlink" title="弊端"></a>弊端</h2>
                  <p>但其实上面的方法有一个不方便的地方，如果我们想要设置非字符串类型的环境变量怎么办呢？比如设置 int 类型、float 类型、list 类型，可能我们的写法就会变成这个样子：</p>
                  <figure class="highlight lua">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import <span class="built_in">os</span></span><br><span class="line">import json</span><br><span class="line"></span><br><span class="line">VAR1 = int(<span class="built_in">os</span>.<span class="built_in">getenv</span>(<span class="string">'VAR1'</span>, <span class="number">1</span>))</span><br><span class="line">VAR2 = float(<span class="built_in">os</span>.<span class="built_in">getenv</span>(<span class="string">'VAR2'</span>, <span class="number">5.5</span>))</span><br><span class="line">VAR3 = json.loads(<span class="built_in">os</span>.<span class="built_in">getenv</span>(<span class="string">'VAR3'</span>))</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>然后设置环境变量的时候就变成这样子：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR1</span>=1</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR2</span>=2.3</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR3</span>=<span class="string">'["1", "2"]'</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样才能成功获取到结果，打印出来结果如下：</p>
                  <figure class="highlight scheme">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">1</span></span><br><span class="line"><span class="number">2.3</span></span><br><span class="line">[<span class="symbol">'1</span>', <span class="symbol">'2</span>']</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>不过看下这个，写法也太奇葩了吧，又是类型转换，又是 json 解析什么的，有没有更好的方法来设置。</p>
                  <h2 id="environs"><a href="#environs" class="headerlink" title="environs"></a>environs</h2>
                  <p>当然有的，下面推荐一个 environs 库，利用它我们可以轻松地设置各种类型的环境变量。 这是一个第三方库，可以通过 pip 来安装：</p>
                  <figure class="highlight cmake">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pip3 <span class="keyword">install</span> environs</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>好，安装之后，我们再来体验一下使用 environs 来设置环境变量的方式。</p>
                  <figure class="highlight dockerfile">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> environs import <span class="keyword">Env</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">env</span> = <span class="keyword">Env</span>()</span><br><span class="line">VAR1 = <span class="keyword">env</span>.int(<span class="string">'VAR1'</span>, <span class="number">1</span>)</span><br><span class="line">VAR2 = <span class="keyword">env</span>.float(<span class="string">'VAR2'</span>, <span class="number">5.5</span>)</span><br><span class="line">VAR3 = <span class="keyword">env</span>.list(<span class="string">'VAR3'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里 environs 直接提供了 int、float、list 等方法，我们就不用再去进行类型转换了。 与此同时，设置环境变量的方式也有所变化：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR1</span>=1</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR2</span>=2.3</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR3</span>=1,2</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里 VAR3 是列表，我们可以直接用逗号分隔开来。 打印结果如下：</p>
                  <figure class="highlight scheme">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">1</span></span><br><span class="line"><span class="number">2.3</span></span><br><span class="line">[<span class="symbol">'1</span>', <span class="symbol">'2</span>']</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h3 id="官方示例"><a href="#官方示例" class="headerlink" title="官方示例"></a>官方示例</h3>
                  <p>下面我们再看一个官方示例，这里示例了一些常见的用法。 首先我们来定义一些环境变量，如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">GITHUB_USER</span>=sloria</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">MAX_CONNECTIONS</span>=100</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">SHIP_DATE</span>=<span class="string">'1984-06-25'</span></span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">TTL</span>=42</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">ENABLE_LOGIN</span>=<span class="literal">true</span></span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">GITHUB_REPOS</span>=webargs,konch,ped</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">COORDINATES</span>=23.3,50.0</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">LOG_LEVEL</span>=DEBUG</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里有字符串、有日期、有日志级别、有字符串列表、有浮点数列表、有布尔。 我们来看下怎么获取，写法如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> environs import Env</span><br><span class="line"></span><br><span class="line">env = Env()</span><br><span class="line">env.read_env()  # read .env file, <span class="keyword">if</span> it exists</span><br><span class="line"><span class="comment"># required variables</span></span><br><span class="line">gh_user = env(<span class="string">"GITHUB_USER"</span>)  # =&gt; <span class="string">'sloria'</span></span><br><span class="line">secret = env(<span class="string">"SECRET"</span>)  # =&gt; raises <span class="builtin-name">error</span> <span class="keyword">if</span> <span class="keyword">not</span> set</span><br><span class="line"></span><br><span class="line"><span class="comment"># casting</span></span><br><span class="line">max_connections = env.int(<span class="string">"MAX_CONNECTIONS"</span>)  # =&gt; 100</span><br><span class="line">ship_date = env.date(<span class="string">"SHIP_DATE"</span>)  # =&gt; datetime.date(1984, 6, 25)</span><br><span class="line">ttl = env.timedelta(<span class="string">"TTL"</span>)  # =&gt; datetime.timedelta(0, 42)</span><br><span class="line">log_level = env.log_level(<span class="string">"LOG_LEVEL"</span>)  # =&gt; logging.DEBUG</span><br><span class="line"></span><br><span class="line"><span class="comment"># providing a default value</span></span><br><span class="line">enable_login = env.bool(<span class="string">"ENABLE_LOGIN"</span>, <span class="literal">False</span>)  # =&gt; <span class="literal">True</span></span><br><span class="line">enable_feature_x = env.bool(<span class="string">"ENABLE_FEATURE_X"</span>, <span class="literal">False</span>)  # =&gt; <span class="literal">False</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># parsing lists</span></span><br><span class="line">gh_repos = env.list(<span class="string">"GITHUB_REPOS"</span>)  # =&gt; [<span class="string">'webargs'</span>, <span class="string">'konch'</span>, <span class="string">'ped'</span>]</span><br><span class="line">coords = env.list(<span class="string">"COORDINATES"</span>, <span class="attribute">subcast</span>=float)  # =&gt; [23.3, 50.0]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>通过观察代码可以发现它提供了这些功能：</p>
                  <ul>
                    <li>通过 env 可以设置必需定义的变量，如果没有定义，则会报错。</li>
                    <li>通过 date、timedelta 方法可以对日期或时间进行转化，转成 datetime.date 或 timedelta 类型。</li>
                    <li>通过 log_level 方法可以对日志级别进行转化，转成 logging 里的日志级别定义。</li>
                    <li>通过 bool 方法可以对布尔类型变量进行转化。</li>
                    <li>通过 list 方法可以对逗号分隔的内容进行 list 转化，并可以通过 subcast 方法对 list 的每个元素进行类型转化。</li>
                  </ul>
                  <p>可以说有了这些方法，定义各种类型的变量都不再是问题了。</p>
                  <h3 id="支持类型"><a href="#支持类型" class="headerlink" title="支持类型"></a>支持类型</h3>
                  <p>总的来说，environs 支持的转化类型有这么多：</p>
                  <ul>
                    <li><code>env.str</code></li>
                    <li><code>env.bool</code></li>
                    <li><code>env.int</code></li>
                    <li><code>env.float</code></li>
                    <li><code>env.decimal</code></li>
                    <li><code>env.list</code> (accepts optional <code>subcast</code> keyword argument)</li>
                    <li><code>env.dict</code> (accepts optional <code>subcast</code> keyword argument)</li>
                    <li><code>env.json</code></li>
                    <li><code>env.datetime</code></li>
                    <li><code>env.date</code></li>
                    <li><code>env.timedelta</code> (assumes value is an integer in seconds)</li>
                    <li><code>env.url</code></li>
                    <li><code>env.uuid</code></li>
                    <li><code>env.log_level</code></li>
                    <li><code>env.path</code> (casts to a <a href="https://docs.python.org/3/library/pathlib.html" target="_blank" rel="noopener"><code>pathlib.Path</code></a>)</li>
                  </ul>
                  <p>这里 list、dict、json、date、url、uuid、path 个人认为都还是比较有用的，另外 list、dict 方法还有一个 subcast 方法可以对元素内容进行转化。 对于 dict、url、date、uuid、path 这里我们来补充说明一下。 下面我们定义这些类型的环境变量：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR_DICT</span>=name=germey,age=25</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR_JSON</span>=<span class="string">'&#123;"name": "germey", "age": 25&#125;'</span></span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR_URL</span>=https://cuiqingcai.com</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR_UUID</span>=762c8d53-5860-4d5d-81bc-210bf2663d0e</span><br><span class="line"><span class="builtin-name">export</span> <span class="attribute">VAR_PATH</span>=/var/py/env</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>需要注意的是，DICT 的解析，需要传入的是逗号分隔的键值对，JSON 的解析是需要传入序列化的字符串。 解析写法如下：</p>
                  <figure class="highlight haskell">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="title">from</span> environs <span class="keyword">import</span> Env</span><br><span class="line"></span><br><span class="line"><span class="title">env</span> = <span class="type">Env</span>()</span><br><span class="line"><span class="type">VAR_DICT</span> = env.dict('<span class="type">VAR_DICT'</span>)</span><br><span class="line"><span class="title">print</span>(<span class="class"><span class="keyword">type</span>(<span class="type">VAR_DICT</span>), <span class="type">VAR_DICT</span>)</span></span><br><span class="line"></span><br><span class="line"><span class="type">VAR_JSON</span> = env.json('<span class="type">VAR_JSON'</span>)</span><br><span class="line"><span class="title">print</span>(<span class="class"><span class="keyword">type</span>(<span class="type">VAR_JSON</span>), <span class="type">VAR_JSON</span>)</span></span><br><span class="line"></span><br><span class="line"><span class="type">VAR_URL</span> = env.url('<span class="type">VAR_URL'</span>)</span><br><span class="line"><span class="title">print</span>(<span class="class"><span class="keyword">type</span>(<span class="type">VAR_URL</span>), <span class="type">VAR_URL</span>)</span></span><br><span class="line"></span><br><span class="line"><span class="type">VAR_UUID</span> = env.uuid('<span class="type">VAR_UUID'</span>)</span><br><span class="line"><span class="title">print</span>(<span class="class"><span class="keyword">type</span>(<span class="type">VAR_UUID</span>), <span class="type">VAR_UUID</span>)</span></span><br><span class="line"></span><br><span class="line"><span class="type">VAR_PATH</span> = env.path('<span class="type">VAR_PATH'</span>)</span><br><span class="line"><span class="title">print</span>(<span class="class"><span class="keyword">type</span>(<span class="type">VAR_PATH</span>), <span class="type">VAR_PATH</span>)</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&lt;class <span class="string">'dict'</span>&gt; &#123;<span class="string">'name'</span>: <span class="string">'germey'</span>, <span class="string">'age'</span>: <span class="string">'25'</span>&#125;</span><br><span class="line">&lt;class <span class="string">'dict'</span>&gt; &#123;<span class="string">'name'</span>: <span class="string">'germey'</span>, <span class="string">'age'</span>: 25&#125;</span><br><span class="line">&lt;class <span class="string">'urllib.parse.ParseResult'</span>&gt; ParseResult(<span class="attribute">scheme</span>=<span class="string">'https'</span>, <span class="attribute">netloc</span>=<span class="string">'cuiqingcai.com'</span>, <span class="attribute">path</span>=<span class="string">''</span>, <span class="attribute">params</span>=<span class="string">''</span>, <span class="attribute">query</span>=<span class="string">''</span>, <span class="attribute">fragment</span>=<span class="string">''</span>)</span><br><span class="line">&lt;class <span class="string">'uuid.UUID'</span>&gt; 762c8d53-5860-4d5d-81bc-210bf2663d0e</span><br><span class="line">&lt;class <span class="string">'pathlib.PosixPath'</span>&gt; /var/py/env</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以看到，它分别给我们转化成了 dict、dict、ParseResult、UUID、PosixPath 类型了。 在代码中直接使用即可。</p>
                  <h3 id="文件读取"><a href="#文件读取" class="headerlink" title="文件读取"></a>文件读取</h3>
                  <p>如果我们的一些环境变量是定义在文件中的，environs 还可以进行读取和加载，默认会读取本地当前运行目录下的 <code>.env</code> 文件。 示例如下：</p>
                  <figure class="highlight dockerfile">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> environs import <span class="keyword">Env</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">env</span> = <span class="keyword">Env</span>()</span><br><span class="line"><span class="keyword">env</span>.read_env()</span><br><span class="line">APP_DEBUG = <span class="keyword">env</span>.bool(<span class="string">'APP_DEBUG'</span>)</span><br><span class="line">APP_ENV = <span class="keyword">env</span>.str(<span class="string">'APP_ENV'</span>)</span><br><span class="line">print(APP_DEBUG)</span><br><span class="line">print(APP_ENV)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>下面我们在 <code>.env</code> 文件中写入如下内容：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">APP_DEBUG</span>=<span class="literal">false</span></span><br><span class="line"><span class="attr">APP_ENV</span>=prod</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="literal">False</span></span><br><span class="line"><span class="string">prod</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>没问题，成功读取。 当然我们也可以自定义读取的文件，如 <code>.env.test</code> 文件，内容如下：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">APP_DEBUG</span>=<span class="literal">false</span></span><br><span class="line"><span class="attr">APP_ENV</span>=test</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>代码则可以这么定义：</p>
                  <figure class="highlight dockerfile">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> environs import <span class="keyword">Env</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">env</span> = <span class="keyword">Env</span>()</span><br><span class="line"><span class="keyword">env</span>.read_env(path=<span class="string">'.env.test'</span>)</span><br><span class="line">APP_DEBUG = <span class="keyword">env</span>.bool(<span class="string">'APP_DEBUG'</span>)</span><br><span class="line">APP_ENV = <span class="keyword">env</span>.str(<span class="string">'APP_ENV'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里就通过 path 传入了定义环境变量的文件路径即可。</p>
                  <h3 id="前缀处理"><a href="#前缀处理" class="headerlink" title="前缀处理"></a>前缀处理</h3>
                  <p>environs 还支持前缀处理，一般来说我们定义一些环境变量，如数据库的连接，可能有 host、port、password 等，但在定义环境变量的时候往往会加上对应的前缀，如 MYSQL_HOST、MYSQL_PORT、MYSQL_PASSWORD 等，但在解析时，我们可以根据前缀进行分组处理，见下面的示例：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># export MYAPP_HOST=lolcathost</span></span><br><span class="line"><span class="comment"># export MYAPP_PORT=3000</span></span><br><span class="line"></span><br><span class="line">with env.prefixed(<span class="string">"MYAPP_"</span>):</span><br><span class="line">    host = env(<span class="string">"HOST"</span>, <span class="string">"localhost"</span>)  # =&gt; <span class="string">'lolcathost'</span></span><br><span class="line">   <span class="built_in"> port </span>= env.int(<span class="string">"PORT"</span>, 5000)  # =&gt; 3000</span><br><span class="line"></span><br><span class="line"><span class="comment"># nested prefixes are also supported:</span></span><br><span class="line"></span><br><span class="line"><span class="comment"># export MYAPP_DB_HOST=lolcathost</span></span><br><span class="line"><span class="comment"># export MYAPP_DB_PORT=10101</span></span><br><span class="line"></span><br><span class="line">with env.prefixed(<span class="string">"MYAPP_"</span>):</span><br><span class="line">    with env.prefixed(<span class="string">"DB_"</span>):</span><br><span class="line">        db_host = env(<span class="string">"HOST"</span>, <span class="string">"lolcathost"</span>)</span><br><span class="line">        db_port = env.int(<span class="string">"PORT"</span>, 10101)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以看到这里通过 with 和 priefixed 方法组合使用即可实现分区处理，这样在每个分组下再赋值到一个字典里面即可。</p>
                  <h3 id="合法性验证"><a href="#合法性验证" class="headerlink" title="合法性验证"></a>合法性验证</h3>
                  <p>有些环境变量的传入是不可预知的，如果传入一些非法的环境变量很可能导致一些难以预料的问题。比如说一些可执行的命令，通过环境变量传进来，如果是危险命令，那么会非常危险。 所以在某些情况下我们需要验证传入的环境变量的有效性，看下面的例子：</p>
                  <figure class="highlight clean">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"># <span class="keyword">export</span> TTL=<span class="number">-2</span></span><br><span class="line"># <span class="keyword">export</span> NODE_ENV=<span class="string">'invalid'</span></span><br><span class="line"># <span class="keyword">export</span> EMAIL=<span class="string">'^_^'</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> environs <span class="keyword">import</span> Env</span><br><span class="line"><span class="keyword">from</span> marshmallow.validate <span class="keyword">import</span> OneOf, Length, Email</span><br><span class="line"></span><br><span class="line">env = Env()</span><br><span class="line"></span><br><span class="line"># simple validator</span><br><span class="line">env.int(<span class="string">"TTL"</span>, validate=lambda n: n &gt; <span class="number">0</span>)</span><br><span class="line"># =&gt; Environment variable <span class="string">"TTL"</span> invalid: [<span class="string">'Invalid value.'</span>]</span><br><span class="line"></span><br><span class="line"># using marshmallow validators</span><br><span class="line">env.str(</span><br><span class="line">    <span class="string">"NODE_ENV"</span>,</span><br><span class="line">    validate=OneOf(</span><br><span class="line">        [<span class="string">"production"</span>, <span class="string">"development"</span>], error=<span class="string">"NODE_ENV must be one of: &#123;choices&#125;"</span></span><br><span class="line">    ),</span><br><span class="line">)</span><br><span class="line"># =&gt; Environment variable <span class="string">"NODE_ENV"</span> invalid: [<span class="string">'NODE_ENV must be one of: production, development'</span>]</span><br><span class="line"></span><br><span class="line"># multiple validators</span><br><span class="line">env.str(<span class="string">"EMAIL"</span>, validate=[Length(min=<span class="number">4</span>), Email()])</span><br><span class="line"># =&gt; Environment variable <span class="string">"EMAIL"</span> invalid: [<span class="string">'Shorter than minimum length 4.'</span>, <span class="string">'Not a valid email address.'</span>]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里，我们通过 validate 方法，并传入一些判断条件。如 NODE_ENV 只允许传入 production 和 develpment 其中之一；EMAIL 必须符合 email 的格式。 这里依赖于 marshmallow 这个库，里面有很多验证条件，大家可以了解下。 如果不符合条件的，会直接抛错，例如：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">marshmallow</span><span class="selector-class">.exceptions</span><span class="selector-class">.ValidationError</span>: <span class="selector-attr">[<span class="string">'Invalid value.'</span>]</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>关于 marshmallow 库的用法，大家可以参考：<a href="https://marshmallow.readthedocs.io/en/stable/，后面我也抽空写一下介绍下" target="_blank" rel="noopener">https://marshmallow.readthedocs.io/en/stable/，后面我也抽空写一下介绍下</a>。 最后再附一点我平时定义环境变量的一些常见写法，如：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> platform</span><br><span class="line"><span class="keyword">from</span> os.path <span class="keyword">import</span> dirname, abspath, <span class="keyword">join</span></span><br><span class="line"><span class="keyword">from</span> environs <span class="keyword">import</span> Env</span><br><span class="line"><span class="keyword">from</span> loguru <span class="keyword">import</span> logger</span><br><span class="line"></span><br><span class="line">env = Env()</span><br><span class="line">env.read_env()</span><br><span class="line"></span><br><span class="line"># definition <span class="keyword">of</span> flags</span><br><span class="line">IS_WINDOWS = platform.<span class="keyword">system</span>().lower() == <span class="string">'windows'</span></span><br><span class="line"></span><br><span class="line"># definition <span class="keyword">of</span> dirs</span><br><span class="line">ROOT_DIR = dirname(dirname(abspath(__file__)))</span><br><span class="line">LOG_DIR = <span class="keyword">join</span>(ROOT_DIR, env.str(<span class="string">'LOG_DIR'</span>, <span class="string">'logs'</span>))</span><br><span class="line"></span><br><span class="line"># definition <span class="keyword">of</span> environments</span><br><span class="line">DEV_MODE, TEST_MODE, PROD_MODE = <span class="string">'dev'</span>, <span class="string">'test'</span>, <span class="string">'prod'</span></span><br><span class="line">APP_ENV = env.str(<span class="string">'APP_ENV'</span>, DEV_MODE).lower()</span><br><span class="line">APP_DEBUG = env.bool(<span class="string">'APP_DEBUG'</span>, <span class="keyword">True</span> <span class="keyword">if</span> APP_ENV == DEV_MODE <span class="keyword">else</span> <span class="keyword">False</span>)</span><br><span class="line">APP_DEV = IS_DEV = APP_ENV == DEV_MODE</span><br><span class="line">APP_PROD = IS_PROD = APP_DEV == PROD_MODE</span><br><span class="line">APP_TEST = IS_TEST = APP_ENV = TEST_MODE</span><br><span class="line"></span><br><span class="line"># redis host</span><br><span class="line">REDIS_HOST = env.str(<span class="string">'REDIS_HOST'</span>, <span class="string">'127.0.0.1'</span>)</span><br><span class="line"># redis port</span><br><span class="line">REDIS_PORT = env.int(<span class="string">'REDIS_PORT'</span>, <span class="number">6379</span>)</span><br><span class="line"># redis <span class="keyword">password</span>, <span class="keyword">if</span> <span class="keyword">no</span> <span class="keyword">password</span>, <span class="keyword">set</span> it <span class="keyword">to</span> <span class="keyword">None</span></span><br><span class="line">REDIS_PASSWORD = env.str(<span class="string">'REDIS_PASSWORD'</span>, <span class="keyword">None</span>)</span><br><span class="line"># redis <span class="keyword">connection</span> string, <span class="keyword">like</span> redis://[<span class="keyword">password</span>]@host:port <span class="keyword">or</span> rediss://[<span class="keyword">password</span>]@host:port</span><br><span class="line">REDIS_CONNECTION_STRING = env.str(<span class="string">'REDIS_CONNECTION_STRING'</span>, <span class="keyword">None</span>)</span><br><span class="line"></span><br><span class="line"># definition <span class="keyword">of</span> api</span><br><span class="line">API_HOST = env.str(<span class="string">'API_HOST'</span>, <span class="string">'0.0.0.0'</span>)</span><br><span class="line">API_PORT = env.int(<span class="string">'API_PORT'</span>, <span class="number">5555</span>)</span><br><span class="line">API_THREADED = env.bool(<span class="string">'API_THREADED'</span>, <span class="keyword">True</span>)</span><br><span class="line"></span><br><span class="line"># definition <span class="keyword">of</span> flags</span><br><span class="line">ENABLE_TESTER = env.bool(<span class="string">'ENABLE_TESTER'</span>, <span class="keyword">True</span>)</span><br><span class="line">ENABLE_GETTER = env.bool(<span class="string">'ENABLE_GETTER'</span>, <span class="keyword">True</span>)</span><br><span class="line">ENABLE_SERVER = env.bool(<span class="string">'ENABLE_SERVER'</span>, <span class="keyword">True</span>)</span><br><span class="line"></span><br><span class="line"># logger</span><br><span class="line">logger.<span class="keyword">add</span>(env.str(<span class="string">'LOG_RUNTIME_FILE'</span>, <span class="string">'runtime.log'</span>), <span class="keyword">level</span>=<span class="string">'DEBUG'</span>, rotation=<span class="string">'1 week'</span>, retention=<span class="string">'20 days'</span>)</span><br><span class="line">logger.<span class="keyword">add</span>(env.str(<span class="string">'LOG_ERROR_FILE'</span>, <span class="string">'error.log'</span>), <span class="keyword">level</span>=<span class="string">'ERROR'</span>, rotation=<span class="string">'1 week'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里定义了一些开发环境、日志路径、数据库连接、API 设置、开关设置等等，是从我之前写的一个代理池项目拿来的，大家可以参考：<a href="https://github.com/Python3WebSpider/ProxyPool" target="_blank" rel="noopener">https://github.com/Python3WebSpider/ProxyPool</a>。 好了，以上就是一些环境变量的定义方法，谢谢大家。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-02-25 01:25:12" itemprop="dateCreated datePublished" datetime="2020-02-25T01:25:12+08:00">2020-02-25</time>
                </span>
                <span id="/8947.html" class="post-meta-item leancloud_visitors" data-flag-title="Python 使用 environs 库来更好地定义环境变量" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>9.1k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>8 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8943.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8943.html" class="post-title-link" itemprop="url">Python 序列化和反序列化库 MarshMallow 的用法</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>在很多情况下，我们会有把 Python 对象进行序列化或反序列化的需求，比如开发 REST API，比如一些面向对象化的数据加载和保存，都会应用到这个功能。 比如这里看一个最基本的例子，这里给到一个 User 的 Class 定义，再给到一个 data 数据，像这样：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">User</span>(<span class="title">object</span>):</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(<span class="keyword">self</span>, name, age)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">self</span>.name = name</span><br><span class="line">        <span class="keyword">self</span>.age = age</span><br><span class="line"></span><br><span class="line">data = [&#123;</span><br><span class="line">    <span class="string">'name'</span>: <span class="string">'Germey'</span>,</span><br><span class="line">    <span class="string">'age'</span>: <span class="number">23</span></span><br><span class="line">&#125;, &#123;</span><br><span class="line">    <span class="string">'name'</span>: <span class="string">'Mike'</span>,</span><br><span class="line">    <span class="string">'age'</span>: <span class="number">20</span></span><br><span class="line">&#125;]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>现在我要把这个 data 快速转成 User 组成的数组，变成这样：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[User(<span class="attribute">name</span>=<span class="string">'Germey'</span>, <span class="attribute">age</span>=23), User(<span class="attribute">name</span>=<span class="string">'Mike'</span>, <span class="attribute">age</span>=20)]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>你会怎么来实现？ 或者我有了上面的列表内容，想要转成一个 JSON 字符串，变成这样：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[&#123;<span class="attr">"name"</span>: <span class="string">"Germey"</span>, <span class="attr">"age"</span>: <span class="number">23</span>&#125;, &#123;<span class="attr">"name"</span>: <span class="string">"Mike"</span>, <span class="attr">"age"</span>: <span class="number">20</span>&#125;]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>你又会怎么操作呢？ 另外如果 JSON 数据里面有各种各样的脏数据，你需要在初始化时验证这些字段是否合法，另外 User 这个对象里面 name、age 的数据类型不同，如何针对不同的数据类型进行针对性的类型转换，这个你有更好的实现方案吗？</p>
                  <h2 id="初步思路"><a href="#初步思路" class="headerlink" title="初步思路"></a>初步思路</h2>
                  <p>之前我写过一篇文章介绍过 attrs 和 cattrs 这两个库，它们二者的组合可以非常方便地实现对象的序列化和反序列化。 譬如这样：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> attr <span class="keyword">import</span> attrs, attrib</span><br><span class="line"><span class="keyword">from</span> cattr <span class="keyword">import</span> structure, unstructure</span><br><span class="line"></span><br><span class="line">@attrs</span><br><span class="line"><span class="keyword">class</span> <span class="keyword">User</span>(<span class="keyword">object</span>):</span><br><span class="line">    <span class="type">name</span> = attrib()</span><br><span class="line">    age = attrib()</span><br><span class="line"></span><br><span class="line">data = &#123;</span><br><span class="line">    <span class="string">'name'</span>: <span class="string">'Germey'</span>,</span><br><span class="line">    <span class="string">'age'</span>: <span class="number">23</span></span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">user</span> = structure(data, <span class="keyword">User</span>)</span><br><span class="line">print(<span class="string">'user'</span>, <span class="keyword">user</span>)</span><br><span class="line"><span class="type">json</span> = unstructure(<span class="keyword">user</span>)</span><br><span class="line">print(<span class="string">'json'</span>, <span class="type">json</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">user</span> <span class="keyword">User</span>(<span class="type">name</span>=<span class="string">'Germey'</span>, age=<span class="number">23</span>)</span><br><span class="line"><span class="type">json</span> &#123;<span class="string">'name'</span>: <span class="string">'Germey'</span>, <span class="string">'age'</span>: <span class="number">23</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>好，这里我们通过 attrs 和 cattrs 这两个库来实现了单个对象的转换。 首先我们要肯定一下 attrs 这个库，它可以极大地简化 Python 类的定义，同时每个字段可以定义多种数据类型。 但 cattrs 这个库就相对弱一些了，如果把 data 换成数组，用 cattrs 还是不怎么好转换的，另外它的 structure 和 unstructure 在某些情景下容错能力较差，所以对于上面的需求，用这两个库搭配起来并不是一个最优的解决方案。 另外数据的校验也是一个问题，attrs 虽然提供了 validator 的参数，但对于多种类型的数据处理的支持并没有那么强大。 所以，我们想要寻求一个更优的解决方案。</p>
                  <h2 id="更优雅的方案"><a href="#更优雅的方案" class="headerlink" title="更优雅的方案"></a>更优雅的方案</h2>
                  <p>这里推荐一个库，叫做 marshmallow，它是专门用来支持 Python 对象和原生数据相互转换的库，如实现 object -&gt; dict，objects -&gt; list, string -&gt; dict, string -&gt; list 等的转换功能，另外它还提供了非常丰富的数据类型转换和校验 API，帮助我们快速实现数据的转换。 要使用 marshmallow 这个库，需要先安装下：</p>
                  <figure class="highlight cmake">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pip3 <span class="keyword">install</span> marshmallow</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>好了之后，我们在之前的基础上定义一个 Schema，如下：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserSchema</span>(<span class="title">Schema</span>):</span></span><br><span class="line">    name = fields.Str()</span><br><span class="line">    age = fields.Integer()</span><br><span class="line"></span><br><span class="line">    @post_load</span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">make</span><span class="params">(<span class="keyword">self</span>, data, **kwargs)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">return</span> User(**data)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>还是之前的数据：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">data</span> = [&#123;</span><br><span class="line">    <span class="string">'name'</span>: <span class="string">'Germey'</span>,</span><br><span class="line">    <span class="string">'age'</span>: <span class="number">23</span></span><br><span class="line">&#125;, &#123;</span><br><span class="line">    <span class="string">'name'</span>: <span class="string">'Mike'</span>,</span><br><span class="line">    <span class="string">'age'</span>: <span class="number">20</span></span><br><span class="line">&#125;]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这时候我们只需要调用 Schema 的 load 事件就好了：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">schema = UserSchema()</span><br><span class="line">users = schema.load(data, <span class="attribute">many</span>=<span class="literal">True</span>)</span><br><span class="line"><span class="builtin-name">print</span>(users)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>输出结果如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[User(<span class="attribute">name</span>=<span class="string">'Germey'</span>, <span class="attribute">age</span>=23), User(<span class="attribute">name</span>=<span class="string">'Mike'</span>, <span class="attribute">age</span>=20)]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样，我们非常轻松地完成了 JSON 到 User List 的转换。 有人说，如果是单个数据怎么办呢，只需要把 load 方法的 many 参数去掉即可：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">data = &#123;</span><br><span class="line">    <span class="string">'name'</span>: <span class="string">'Germey'</span>,</span><br><span class="line">    <span class="string">'age'</span>: 23</span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line">schema = UserSchema()</span><br><span class="line">user = schema.load(data)</span><br><span class="line"><span class="builtin-name">print</span>(user)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>输出结果：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">User(<span class="attribute">name</span>=<span class="string">'Germey'</span>, <span class="attribute">age</span>=23)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>当然，这仅仅是一个反序列化操作，我们还可以正向进行序列化，以及使用各种各样的验证条件。 下面我们再来看看吧。</p>
                  <h2 id="更方便的序列化"><a href="#更方便的序列化" class="headerlink" title="更方便的序列化"></a>更方便的序列化</h2>
                  <p>上面的例子我们实现了序列化操作，输出了 users 为：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[User(<span class="attribute">name</span>=<span class="string">'Germey'</span>, <span class="attribute">age</span>=23), User(<span class="attribute">name</span>=<span class="string">'Mike'</span>, <span class="attribute">age</span>=20)]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>有了这个数据，我们也能轻松实现序列化操作。 序列化操作，使用 dump 方法即可</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">result = schema.dump(users, <span class="attribute">many</span>=<span class="literal">True</span>)</span><br><span class="line"><span class="builtin-name">print</span>(<span class="string">'result'</span>, result)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">result</span> <span class="selector-attr">[&#123;<span class="string">'age'</span>: 23, <span class="string">'name'</span>: <span class="string">'Germey'</span>&#125;, &#123;<span class="string">'age'</span>: 20, <span class="string">'name'</span>: <span class="string">'Mike'</span>&#125;]</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>由于是 List，所以 dump 方法需要加一个参数 many 为 True。 当然对于单个对象，直接使用 dump 同样是可以的：</p>
                  <figure class="highlight stylus">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">result = schema.dump(user)</span><br><span class="line"><span class="function"><span class="title">print</span><span class="params">(<span class="string">'result'</span>, result)</span></span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight puppet">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">result</span> &#123;<span class="string">'name'</span>: <span class="string">'Germey'</span>, <span class="string">'age'</span>: <span class="number">23</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样的话，单个、多个对象的序列化也不再是难事。 经过上面的操作，我们完成了 object 到 dict 或 list 的转换，即：</p>
                  <figure class="highlight ocaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">object</span> &lt;-&gt; dict</span><br><span class="line">objects &lt;-&gt; <span class="built_in">list</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h2 id="验证"><a href="#验证" class="headerlink" title="验证"></a>验证</h2>
                  <p>当然，上面的功能其实并不足以让你觉得 marshmallow 有多么了不起，其实就是一个对象到基本数据的转换嘛。但肯定不止这些，marshmallow 还提供了更加强大啊功能，比如说验证，Validation。 比如这里我们将 age 这个字段设置为 hello，它无法被转换成数值类型，所以肯定会报错，样例如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">data = &#123;</span><br><span class="line">    <span class="string">'name'</span>: <span class="string">'Germey'</span>,</span><br><span class="line">    <span class="string">'age'</span>: <span class="string">'hello'</span></span><br><span class="line">&#125;</span><br><span class="line"></span><br><span class="line"><span class="keyword">from</span> marshmallow <span class="keyword">import</span> ValidationError</span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    schema = UserSchema()</span><br><span class="line">    user, errors = schema.load(data)</span><br><span class="line">    print(user, errors)</span><br><span class="line"><span class="keyword">except</span> ValidationError <span class="keyword">as</span> e:</span><br><span class="line">    print(<span class="string">'e.message'</span>, e.messages)</span><br><span class="line">    print(<span class="string">'e.valid_data'</span>, e.valid_data)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里如果加载报错，我们可以直接拿到 Error 的 messages 和 valid_data 对象，它包含了错误的信息和正确的字段结果，运行结果如下：</p>
                  <figure class="highlight puppet">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">e.<span class="keyword">message</span> &#123;<span class="string">'age'</span>: [<span class="string">'Not a valid integer.'</span>]&#125;</span><br><span class="line"><span class="keyword">e</span>.<span class="keyword">valid_data</span> &#123;<span class="string">'name'</span>: <span class="string">'Germey'</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>因此，比如我们想要开发一个功能，比如用户注册，表单信息就是提交过来的 data，我们只需要过一遍 Validation，就可以轻松得知哪些数据符合要求，哪些不符合要求，接着再进一步进行处理。 当然验证功能肯定不止这一些，我们再来感受一下另一个示例：</p>
                  <figure class="highlight xquery">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">from pprint <span class="keyword">import</span> pprint</span><br><span class="line">from marshmallow <span class="keyword">import</span> Schema, fields, <span class="keyword">validate</span>, ValidationError</span><br><span class="line"></span><br><span class="line">class UserSchema(Schema):</span><br><span class="line">   <span class="built_in"> name</span> = fields.Str(<span class="keyword">validate</span>=<span class="keyword">validate</span>.Length<span class="built_in">(min</span>=<span class="number">1</span>))</span><br><span class="line">    permission = fields.Str(<span class="keyword">validate</span>=<span class="keyword">validate</span>.OneOf([<span class="string">'read'</span>, <span class="string">'write'</span>, <span class="string">'admin'</span>]))</span><br><span class="line">    age = fields.Int(<span class="keyword">validate</span>=<span class="keyword">validate</span>.Range<span class="built_in">(min</span>=<span class="number">18</span>,<span class="built_in"> max</span>=<span class="number">40</span>))</span><br><span class="line"></span><br><span class="line">in_data = &#123;<span class="string">'name'</span>: <span class="string">''</span>, <span class="string">'permission'</span>: <span class="string">'invalid'</span>, <span class="string">'age'</span>: <span class="number">71</span>&#125;</span><br><span class="line">try:</span><br><span class="line">    UserSchema().load(in_data)</span><br><span class="line"><span class="keyword">except</span> ValidationError <span class="keyword">as</span> err:</span><br><span class="line">    pprint(err.messages)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>比如这里的 validate 字段，我们分别校验了 name、permission、age 三个字段，校验方式各不相同。 如 name 我们要判断其最小值为 1，则使用了 Length 对象。permission 必须要是几个字符串之一，这里又使用了 OneOf 对象，age 又必须是介于某个范围之间，这里就使用了 Range 对象。 下面我们故意传入一些错误的数据，看下运行结果：</p>
                  <figure class="highlight applescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;'age': ['Must be <span class="keyword">greater than</span> <span class="keyword">or</span> <span class="keyword">equal</span> <span class="keyword">to</span> <span class="number">18</span> <span class="keyword">and</span> <span class="keyword">less than or equal</span> <span class="keyword">to</span> <span class="number">40.</span>'],</span><br><span class="line"> '<span class="built_in">name</span>': ['Shorter than minimum <span class="built_in">length</span> <span class="number">1.</span>'],</span><br><span class="line"> 'permission': ['Must be one <span class="keyword">of</span>: <span class="built_in">read</span>, <span class="built_in">write</span>, admin.']&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以看到，这里也返回了数据验证的结果，对于不符合条件的字段，一一进行说明。 另外我们也可以自定义验证方法：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">from marshmallow import Schema, fields, ValidationError</span><br><span class="line"></span><br><span class="line">def validate<span class="constructor">_quantity(<span class="params">n</span>)</span>:</span><br><span class="line">    <span class="keyword">if</span> n &lt; <span class="number">0</span>:</span><br><span class="line">        raise <span class="constructor">ValidationError('Quantity <span class="params">must</span> <span class="params">be</span> <span class="params">greater</span> <span class="params">than</span> 0.')</span></span><br><span class="line">    <span class="keyword">if</span> n &gt; <span class="number">30</span>:</span><br><span class="line">        raise <span class="constructor">ValidationError('Quantity <span class="params">must</span> <span class="params">not</span> <span class="params">be</span> <span class="params">greater</span> <span class="params">than</span> 30.')</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="constructor">ItemSchema(Schema)</span>:</span><br><span class="line">    quantity = fields.<span class="constructor">Integer(<span class="params">validate</span>=<span class="params">validate_quantity</span>)</span></span><br><span class="line"></span><br><span class="line">in_data = &#123;'quantity': <span class="number">31</span>&#125;</span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    result = <span class="constructor">ItemSchema()</span>.load(in_data)</span><br><span class="line">except ValidationError <span class="keyword">as</span> err:</span><br><span class="line">    print(err.messages)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>通过自定义方法，同样可以实现更灵活的验证，运行结果：</p>
                  <figure class="highlight prolog">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="string">'quantity'</span>: [<span class="string">'Quantity must not be greater than 30.'</span>]&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>对于上面的例子，还有更优雅的写法：</p>
                  <figure class="highlight reasonml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">from marshmallow import fields, Schema, validates, ValidationError</span><br><span class="line"></span><br><span class="line"><span class="keyword">class</span> <span class="constructor">ItemSchema(Schema)</span>:</span><br><span class="line">    quantity = fields.<span class="constructor">Integer()</span></span><br><span class="line"></span><br><span class="line">    @validates('quantity')</span><br><span class="line">    def validate<span class="constructor">_quantity(<span class="params">self</span>, <span class="params">value</span>)</span>:</span><br><span class="line">        <span class="keyword">if</span> value &lt; <span class="number">0</span>:</span><br><span class="line">            raise <span class="constructor">ValidationError('Quantity <span class="params">must</span> <span class="params">be</span> <span class="params">greater</span> <span class="params">than</span> 0.')</span></span><br><span class="line">        <span class="keyword">if</span> value &gt; <span class="number">30</span>:</span><br><span class="line">            raise <span class="constructor">ValidationError('Quantity <span class="params">must</span> <span class="params">not</span> <span class="params">be</span> <span class="params">greater</span> <span class="params">than</span> 30.')</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>通过定义方法并用 validates 修饰符，使得代码的书写更加简洁。</p>
                  <h2 id="必填字段"><a href="#必填字段" class="headerlink" title="必填字段"></a>必填字段</h2>
                  <p>如果要想定义必填字段，只需要在 fields 里面加入 required 参数并设置为 True 即可，另外我们还可以自定义错误信息，使用 error_messages 即可，例如：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> pprint <span class="keyword">import</span> pprint</span><br><span class="line"><span class="keyword">from</span> marshmallow <span class="keyword">import</span> Schema, fields, ValidationError</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserSchema</span><span class="params">(Schema)</span>:</span></span><br><span class="line">    name = fields.String(required=<span class="literal">True</span>)</span><br><span class="line">    age = fields.Integer(required=<span class="literal">True</span>, error_messages=&#123;<span class="string">'required'</span>: <span class="string">'Age is required.'</span>&#125;)</span><br><span class="line">    city = fields.String(</span><br><span class="line">        required=<span class="literal">True</span>,</span><br><span class="line">        error_messages=&#123;<span class="string">'required'</span>: &#123;<span class="string">'message'</span>: <span class="string">'City required'</span>, <span class="string">'code'</span>: <span class="number">400</span>&#125;&#125;,</span><br><span class="line">    )</span><br><span class="line">    email = fields.Email()</span><br><span class="line"></span><br><span class="line"><span class="keyword">try</span>:</span><br><span class="line">    result = UserSchema().load(&#123;<span class="string">'email'</span>: <span class="string">'foo@bar.com'</span>&#125;)</span><br><span class="line"><span class="keyword">except</span> ValidationError <span class="keyword">as</span> err:</span><br><span class="line">    pprint(err.messages)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h2 id="默认字段"><a href="#默认字段" class="headerlink" title="默认字段"></a>默认字段</h2>
                  <p>对于序列化和反序列化字段，marshmallow 还提供了默认值，而且区分得非常清楚！如 missing 则是在反序列化时自动填充的数据，default 则是在序列化时自动填充的数据。 例如：</p>
                  <figure class="highlight haskell">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="title">from</span> marshmallow <span class="keyword">import</span> Schema, fields</span><br><span class="line"><span class="keyword">import</span> datetime <span class="keyword">as</span> dt</span><br><span class="line"><span class="keyword">import</span> uuid</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="type">UserSchema</span>(<span class="type">Schema</span>):</span></span><br><span class="line"><span class="class">    id = fields.<span class="type">UUID</span>(<span class="title">missing</span>=<span class="title">uuid</span>.<span class="title">uuid1</span>)</span></span><br><span class="line"><span class="class">    birthdate = fields.<span class="type">DateTime</span>(<span class="title">default</span>=<span class="title">dt</span>.<span class="title">datetime</span>(2017, 9, 29))</span></span><br><span class="line"><span class="class"></span></span><br><span class="line"><span class="class">print(<span class="type">UserSchema</span>().load(&#123;&#125;))</span></span><br><span class="line"><span class="class">print(<span class="type">UserSchema</span>().dump(&#123;&#125;))</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里我们都是定义的空数据，分别进行序列化和反序列化，运行结果如下：</p>
                  <figure class="highlight 1c">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;'id': UUID('06aa384a-570c-11ea-<span class="number">9869</span>-a<span class="number">0999</span>b0d<span class="number">6843</span>')&#125;</span><br><span class="line">&#123;'birthdate': '<span class="number">2017-09-29</span>T00:00:00'&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以看到，在没有真实值的情况下，序列化和反序列化都是用了默认值。 这个真的是解决了我之前在 cattrs 序列化和反序列化时候的痛点啊！</p>
                  <h2 id="指定属性名"><a href="#指定属性名" class="headerlink" title="指定属性名"></a>指定属性名</h2>
                  <p>在序列化时，Schema 对象会默认使用和自身定义相同的 fields 属性名，当然也可以自定义，如：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">class UserSchema(Schema):</span><br><span class="line">    name = fields.String()</span><br><span class="line">    email_addr = fields.String(<span class="attribute">attribute</span>=<span class="string">'email'</span>)</span><br><span class="line">    date_created = fields.DateTime(<span class="attribute">attribute</span>=<span class="string">'created_at'</span>)</span><br><span class="line"></span><br><span class="line">user = User(<span class="string">'Keith'</span>, <span class="attribute">email</span>=<span class="string">'keith@stones.com'</span>)</span><br><span class="line">ser = UserSchema()</span><br><span class="line">result, errors = ser.dump(user)</span><br><span class="line">pprint(result)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight 1c">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;'name': 'Keith',</span><br><span class="line"> 'email_addr': 'keith@stones.com',</span><br><span class="line"> 'date_created': '<span class="number">2014-08-17</span>T14:58:57.<span class="number">600623</span>+00:00'&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>反序列化也是一样，例如：</p>
                  <figure class="highlight kotlin">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">UserSchema</span></span>(Schema):</span><br><span class="line">    name = fields.String()</span><br><span class="line">    email = fields.Email(load_from=<span class="string">'emailAddress'</span>)</span><br><span class="line"></span><br><span class="line"><span class="keyword">data</span> = &#123;</span><br><span class="line">    <span class="string">'name'</span>: <span class="string">'Mike'</span>,</span><br><span class="line">    <span class="string">'emailAddress'</span>: <span class="string">'foo@bar.com'</span></span><br><span class="line">&#125;</span><br><span class="line">s = UserSchema()</span><br><span class="line">result, errors = s.load(<span class="keyword">data</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight awk">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="string">'name'</span>: <span class="string">u'Mike'</span>,</span><br><span class="line"> <span class="string">'email'</span>: <span class="string">'foo@bar.com'</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <h2 id="嵌套属性"><a href="#嵌套属性" class="headerlink" title="嵌套属性"></a>嵌套属性</h2>
                  <p>对于嵌套属性，marshmallow 当然也不在话下，这也是让我觉得 marshmallow 非常好用的地方，例如：</p>
                  <figure class="highlight haskell">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="title">from</span> datetime <span class="keyword">import</span> date</span><br><span class="line"><span class="title">from</span> marshmallow <span class="keyword">import</span> Schema, fields, pprint</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="type">ArtistSchema</span>(<span class="type">Schema</span>):</span></span><br><span class="line"><span class="class">    name = fields.<span class="type">Str</span>()</span></span><br><span class="line"><span class="class"></span></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="type">AlbumSchema</span>(<span class="type">Schema</span>):</span></span><br><span class="line"><span class="class">    title = fields.<span class="type">Str</span>()</span></span><br><span class="line"><span class="class">    release_date = fields.<span class="type">Date</span>()</span></span><br><span class="line"><span class="class">    artist = fields.<span class="type">Nested</span>(<span class="type">ArtistSchema</span>())</span></span><br><span class="line"><span class="class"></span></span><br><span class="line"><span class="class">bowie = dict(<span class="title">name</span>='<span class="type">David</span> <span class="type">Bowie</span>')</span></span><br><span class="line"><span class="class">album = dict(<span class="title">artist</span>=<span class="title">bowie</span>, <span class="title">title</span>='<span class="type">Hunky</span> <span class="type">Dory</span>', <span class="title">release_date</span>=<span class="title">date</span>(1971, 12, 17))</span></span><br><span class="line"><span class="class"></span></span><br><span class="line"><span class="class">schema = <span class="type">AlbumSchema</span>()</span></span><br><span class="line"><span class="class">result = schema.dump(<span class="title">album</span>)</span></span><br><span class="line"><span class="class">pprint(<span class="title">result</span>, <span class="title">indent</span>=2)</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就能充分利用好对象关联外键来方便地实现很多关联功能。 以上介绍的内容基本算在日常的使用中是够用了，当然以上都是一些基本的示例，对于更多功能，可以参考 marchmallow 的官方文档：<a href="https://marshmallow.readthedocs.io/en/stable/，强烈推荐大家用起来" target="_blank" rel="noopener">https://marshmallow.readthedocs.io/en/stable/，强烈推荐大家用起来</a>。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-02-25 01:21:41" itemprop="dateCreated datePublished" datetime="2020-02-25T01:21:41+08:00">2020-02-25</time>
                </span>
                <span id="/8943.html" class="post-meta-item leancloud_visitors" data-flag-title="Python 序列化和反序列化库 MarshMallow 的用法" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>7.5k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>7 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8939.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 福利专区 <i class="label-arrow"></i>
                  </a>
                  <a href="/8939.html" class="post-title-link" itemprop="url">东鸽送3台｜做开发没有云服务器怎么行？</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>随着云计算和虚拟技术的发展，主机业务从虚拟主机逐步发展到独享云服务器。我们 IT 人对服务器的需求是很强烈的，无论你是后端研发、前端开发、云计算、大数据、架构、数据存储、运维还是产品经理，每个人手上多多少少都会有几台云服务器。 <img src="http://can.sfhfpc.com/sfhfpc/20200220191443.jpg" alt=""> 这些云服务器被用做测试用机、学习用机或者正式生产用机，有些朋友会在上面搭建博客，有些朋友在上面搭建 API。东鸽自己也手握几台服务器： <img src="http://can.sfhfpc.com/sfhfpc/20200220182953.jpg" alt=""> 其中有 3 台用作实际的生产，新书《<a href="https://item.jd.com/12794078.html" target="_blank" rel="noopener">Python3 反爬虫原理与绕过实战</a>》的<a href="http://www.porters.vip/" target="_blank" rel="noopener">在线练习平台</a>、<a href="https://bbs.nightteam.cn/" target="_blank" rel="noopener">夜幕爬虫安全论坛</a>和我的<a href="http://www.sfhfpc.com/" target="_blank" rel="noopener">个人博客</a>。另外几台用来做测试、数据合作和练习。 练习的话，像平时学习 MongoDB 数据库、Redis 数据库和 Mysql 数据库的时候，我会在服务器上安装对应的应用，启动服务后在自己的电脑上连接，这样能得到与生产环境一样的体验。学习 Linux 和 Shell 脚本的时候也会用云服务器进行练习，这样既能够确保练习效果，又避免了对本机电脑的干扰。 <strong>你总不能在自己的电脑上折腾来折腾去吧，万一搞坏了，工作咋办？</strong> 数据合作，那是个人业务上的事了。数据用云端数据库的方式交付给客户，不需要其他传输媒介，轻便快捷，没有延时风险。我这边把数据存储到服务器，客户从服务器上拉取数据。 <img src="https://www.rabbitmq.com/img/tutorials/python-one.png" alt=""> <strong>一个普通的生产者消费者模式就诞生了。</strong>如果数据业务体量比较大，或者说数据业务的出口比较多，那我可能会借用消息中间件来进行调节。 <img src="https://www.rabbitmq.com/img/tutorials/python-four.png" alt=""> 为了保证数据的完整性和可用性，我可能会对服务器进行镜像，那么我就需要 2～3 台服务器。假设每台服务器的成本是 300元/年，数据合作业务的收入是 3W/年，那真是四两拨千斤了。 要合理利用资源，让资源为我们生<strong>小钱钱</strong>！ 服务器配置跟具体业务需求有关，用于测试的服务器通常是 1 核 2G 1M；博客和论坛的服务器带宽稍大一些，带宽通常是 3M ；用于数据合作的服务器内存较大，一般需要为 4G～8G； 除了服务器之外，<strong>华为云</strong>还有很多<strong>神奇的业务</strong>。我觉得<strong>黑科技</strong>真的是太多了。 <img src="https://www.huaweicloud.com/content/dam/cloudbu-site/archive/china/zh-cn/product/enterprise_intelligence/modelarts/images/modelarts-learn-slide1.png" alt=""> 上次我用华为云的深度学习一体化服务 ModelArts 在线实现了图像深度学习中的图片标注、图片分类预测和模型部署+生成 API，感觉很流畅。 <img src="http://can.sfhfpc.com/sfhfpc/20200221172409.jpg" alt=""> 鼠标点点点，配置一丢丢参数，也不用自己写代码，也不用弄 Web 框架，唰唰唰地几下，就可以调用了。 崔庆才崔哥用 ModelArts 实现了爬虫工程师常用到的拼图验证码缺口识别 API。 <img src="http://can.sfhfpc.com/sfhfpc/20200221172230.jpg" alt=""> 样本准备了小小几十个，训练完成后的识别准确率很高，这样我们就不用担心缺口位置定位的问题了。 <img src="http://can.sfhfpc.com/sfhfpc/20200221172556.jpg" alt=""> 服务器数量多，开销自然就大。如无必要，通常我是不会购买服务器的，但每当云服务器厂商有活动时我都会做一些购买计划。例如我今年打算增加数据业务，并且学习架构方面的知识，那我至少得再买 5 台服务器。趁着华为开年采购季活动，购买一台服务器的成本最低 79元/年。除此之外，华为云还有数据库专场活动、域名建站专场活动、云安全专场活动在等着我们，买买买！ <img src="http://can.sfhfpc.com/sfhfpc/20200220185546.jpeg" alt=""> 大家相识已久，东鸽感谢大家关注和支持，这次华为云开年采购季活动我自掏腰包购买 3 台 1 核 2G 1M 的服务器（1年期）送给各位粉丝。大家扫码即可参与抽奖，到期自动开奖。 <strong>参与要求</strong>：参与活动时必须转发上方华为云开年采购季海报，领奖时小编会检查的哦。 <img src="http://can.sfhfpc.com/sfhfpc/20200220190807.jpeg" alt=""></p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/韦世东学算法和反爬虫" class="author" itemprop="url" rel="index">韦世东学算法和反爬虫</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-02-24 10:06:03" itemprop="dateCreated datePublished" datetime="2020-02-24T10:06:03+08:00">2020-02-24</time>
                </span>
                <span id="/8939.html" class="post-meta-item leancloud_visitors" data-flag-title="东鸽送3台｜做开发没有云服务器怎么行？" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>1.3k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>1 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8893.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/8893.html" class="post-title-link" itemprop="url">推荐个好用的书签工具</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>今天刚发现了一个我觉得不错的工具，介绍给大家，不是打广告哈，是真心推荐。 在推荐之前，问大家一个问题：</p>
                  <ul>
                    <li>大家平常遇到不错的网站或文章，会用什么方式收藏？Chrome 书签？</li>
                    <li>现在你们 Chrome 书签里面啥样子？乱不乱？</li>
                    <li>如果我让你们快速从书签里面找出一个曾经收藏过网站，你要花多久？</li>
                    <li>如果你在手机上用其他的浏览器，比如 Safari、微博上看到了一个不错的网站，怎么存？</li>
                    <li>存到了 Safari 上面，又怎么和电脑上的 Chrome 书签合并？之后还能找到吗？</li>
                  </ul>
                  <p>在这个终身学习的时代，我们需要保存和收藏的东西太多太多了。好的网站，好的博文，好的软件，都需要存下来。那么怎么存？这是个问题。 我最早也用过 Chrome 书签，但是这有个毛病，怎么全平台同步？我想在手机（iPhone）上看我的书签内容，难道我还要专门下个 Chrome？另外书签的整理和搜索也是个问题，实在是让我喜欢不起来。 我在选择软件都会追求全平台云同步。比如时间规划软件，我会用「滴答清单」；SSH 软件，我会用「Termius」；翅膀软件我会用「Surge」，为的都是解决一些跨终端问题，它们都是 iPhone、iPad、Mac、Windows、Web 全部平台同步的，有的甚至还支持浏览器插件或者 Apple Watch 等等，这样我们不论切换到哪个终端，都会非常方便地同步数据。</p>
                  <h2 id="Pocket"><a href="#Pocket" class="headerlink" title="Pocket"></a>Pocket</h2>
                  <p>所以，对于上面说的这个问题，怎么来收藏链接，有什么好用的 App 呢？这个最初是选择了「Pocket」。 <img src="https://qiniu.cuiqingcai.com/2020-02-03-142941.png" alt="image-20200203222939945"> 「Pocket」这个软件有一个不错的特性，那就是跨平台，我可以在浏览器、Mac、iPhone、iPad 上使用，看到不错的网站，调出插件或者点击「分享到 Pocket」就可以存进去了，这样就解决了多平台同步问题，另外 「Pocket」还能设置一些标签等等，然后每周或定期我再翻出来整理整理。 说实话，我用「Pocket」好几年了，但总觉得一直达不到我心中完美的标准，怎么说呢？下面总结一下：</p>
                  <h3 id="跨平台"><a href="#跨平台" class="headerlink" title="跨平台"></a>跨平台</h3>
                  <p>跨平台的支持确实挺好的，这也是我选择「Pocket」的重要原因，支持 Web、Mac、Windows、iPhone、iPad 、Android 各大平台，另外还提供了多款浏览器插件，全平台同步。 光这一点就干倒了很多竞争者了。 然而，不好的也有很多。</p>
                  <h3 id="界面"><a href="#界面" class="headerlink" title="界面"></a>界面</h3>
                  <p>首先，Mac 的界面太丑了，多少年了，我一直盼着能改好看点，结果界面一直没有大的改观。这谁受得住啊？ 截图看看： <img src="https://qiniu.cuiqingcai.com/2020-02-03-113030.png" alt="image-20200203193029173"> 哎，一眼难尽啊，左边浓浓的拟物化风格，右边自作聪明出了个阅读模式，也就是自动提取正文内容，结果把文章格式整的这么乱。</p>
                  <h3 id="功能"><a href="#功能" class="headerlink" title="功能"></a>功能</h3>
                  <p>怎么说呢？其实不能追求太多，我最初也只是想找一个存书签的软件。但它的一些分类和标签做的体验并不友好。另外如果我想存文件、存图片，那几乎是不可能的了。 另外这个页面布局吧，实在是让我难以恭维。我不能一目了然地看到我的所有分类和分类层级、标签等等内容。 另外「Pocket」里面还增加了「发现」、「活动」、「资料」三个选项卡，总体来说有点社交性质，它让我去关注点别人，然后看到别人的收藏动态，其中两个选项卡几乎都是常年没啥有效内容的，觉得这些功能有点鸡肋，不足以上升到能和我的收藏列表一个级别。所以我基本上就只开第一个选项卡「我的列表」，只有这里才是我真正想要的收藏列表。 看下图吧，只有第一个选项卡才是收藏列表，第二个就是「发现」，第三个「活动」，最后一个「资料」，后三个基本上没啥卵用，没关注几个人，第三个基本上是常年空着的状态。 <img src="https://qiniu.cuiqingcai.com/2020-02-03-144032.png" alt="IMG_2326"> 所以说，这软件的界面、操作和功能，只能说我不喜欢。不喜欢就不要将就，我将就了这么久，最后还是要放弃它。</p>
                  <h2 id="Raindrop-io"><a href="#Raindrop-io" class="headerlink" title="Raindrop.io"></a>Raindrop.io</h2>
                  <p>所以近期我就一直在找一款能替换掉「Pocket」的纯粹的内容收集软件，这么几个原则吧：</p>
                  <ul>
                    <li>跨平台，必须支持所有终端，包括 Windows、Mac、iPhone、iPad、Android、Web 并提供各个浏览器插件。</li>
                    <li>功能明确，能方便地管理分类、标签、收藏等内容，且不要有一些其他鸡肋的功能。</li>
                    <li>好看，好看，好看！我还是很注重界面和美观的，不论是图标还是内页，不优雅的界面会让我觉得很不爽。（之前找软件的时候因为某些软件的图标设计得不好看而被我 Pass 了）。</li>
                  </ul>
                  <p>好了，搜啊搜，找到这么一款软件—— Raindrop.io，感觉不错，看到首页的介绍，被吸引到了！ <img src="https://qiniu.cuiqingcai.com/2020-02-03-143755.png" alt=""> 大家可以看视频来感受一下：<a href="https://up.raindrop.io/web/marketing/intro.mp4" target="_blank" rel="noopener">https://up.raindrop.io/web/marketing/intro.mp4</a> 可以说首先界面真的吸引到我了，而且左侧的导航分类、收藏夹的管理非常清晰，页面布局也很清晰，甚至支持文件、图片等格式的收藏！另外它同样也支持全平台，完全符合我的需求。 然后我就下载下来了各个平台都试了试，首先试了试它的一些基础功能，比如收藏夹的管理，然后添加上一些不错的内容。 初步整理成下图这么个样子： <img src="https://qiniu.cuiqingcai.com/2020-02-03-141955.png" alt="image-20200203221953942"> 另外内容还支持各种其他的布局，如列表式： <img src="https://qiniu.cuiqingcai.com/2020-02-03-142113.png" alt="image-20200203222112657"> 瀑布流式： <img src="https://qiniu.cuiqingcai.com/2020-02-03-135545.png" alt="image-20200203215543721"> 舒服了。 说回功能，这里我先分了几个分组，为「网站」、「代码」、「图片」，我估计我用到的最多的肯定是「网站」这个分组，用来存各种链接的，「代码」和「图片」是为了测试它的上传文件和图片功能而加的，可以用来存代码文件和图片。 分组建好之后，我们可以在分组里面添加收藏夹，看图里面每行前面有个小图标的就是一个收藏夹。没错，每个收藏夹都可以自定义它的小图标，这个功能真的是增色不少！ 它提供了非常多的小图标，看： <img src="https://qiniu.cuiqingcai.com/2020-02-03-140212.png" alt="image-20200203220210879"> 这个功能，我觉得简直不能更赞！另外这些小图标还可以自己上传，所以你看图里面的 GitHub 图标、Python 图标，都是我从 icons8 上面找到并上传的，毫无违和感！ 另外每一个收藏它都会自动生成一张封面图，会自动截取网站当前页面的内容，或者也可以自定义上传图片或者自定义截屏，最后每个收藏项都会变成一张张卡片。 当然添加标签页不在话下。 <img src="https://qiniu.cuiqingcai.com/2020-02-03-143951.png" alt="image-20200203220829929"> 另外浏览器的插件也做的很精致，提供了 Mini Application 和极简模式，收藏一个页面只需要点一下这个图标就收藏好了。如果是高级版的账号，还支持自动分类。 <img src="https://qiniu.cuiqingcai.com/2020-02-03-141017.png" alt="image-20200203221016017"> 手机和 iPad 上面的软件我也试了，功能基本类似。怎么保存呢？比如我的 iPhone 上可以把这个软件放到分享的 App 列表里面，这样在浏览器里面点击「分享」，然后选「Raindrop.io」就好了，它会自动弹出一个窗口，让我们选择分类或加标签，体验很不错。 <img src="https://qiniu.cuiqingcai.com/2020-02-03-141751.png" alt="IMG_2327"> 嗯，总之整体体验下来，非常好用。 另外它还支持自动导入书签或从其他的收藏软件里面迁移，大家如果一些网站保存在书签里面的话，如果用了这个，可以快速导入进来，非常方便！ <img src="https://qiniu.cuiqingcai.com/2020-02-03-142317.png" alt="image-20200203222316105"> 另外还有一些高级的功能大家再探索吧。怎么感觉越写越像个广告文了，但确实这不是广告文，它确实很好用，在这强推给大家！希望它能帮助大家方便管理各种资源，什么博客、公众号、干货、微博、图片、文件统统可以保存到这里并明确清晰地归类啦！</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-02-10 01:37:28" itemprop="dateCreated datePublished" datetime="2020-02-10T01:37:28+08:00">2020-02-10</time>
                </span>
                <span id="/8893.html" class="post-meta-item leancloud_visitors" data-flag-title="推荐个好用的书签工具" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>2.7k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8891.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 个人随笔 <i class="label-arrow"></i>
                  </a>
                  <a href="/8891.html" class="post-title-link" itemprop="url">关于开会的一些思考</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>今天一个偶然的机会，在群里看到了一个推送，是来自一个软件「飞书」的公开课，它讲如何使用飞书，以及如何提高团队的协作效率，我就去听了一下。 头条是飞书开发的，整个 Talk 其实讲了挺多的关于飞书的使用，其实和很多软件的功能大体上是相同的，如文件共享、文档协作、任务分配、聊天沟通等等，像行业内挺多软件，如 Worktile、TAPD、Teambition 等等也都比较完善了，所以一些类似的功能我就不展开说了。 不过其中有一个点，让我听了之后深受启发，那就是如何提高开会效率。 我也参加过很多会议了，包括研究生期间跟着导师出去汇报、开实验室小组会或者公司内部开大会。其中有的内容是讨论具体的实施方案，有的一些时候是单纯的分享或者 sync 进度。对于后者的话，其实没什么问题，就是一个人分享或者快速跟其他人沟通，我想大部分情况下大家都是类似的。但是对于前者，我就觉得一些会议效率太低了，甚至说价值并不高，那么这里就主要说说这种会议。 比如有些会议，尤其是大会，比如十几二十人围在一起开会讨论方案，参与的人很多很杂，有的人甚至还是和会议相关度不高的人或者本身对会议主题完全不了解的人。这时候可能有一个人主持会先说说这个会是干嘛的，开这个会要讨论点什么，说了十分钟之后，一些人才大体上明白这个会在做什么。然后后面就是后面开始讨论方案了，大家也没有整个的时间把控，不知道一共可能讨论多久，大家你一句我一句，有时候扯着扯着扯远了，最后一个会能开上一个半甚至两个多小时，然后会议结束了之后，大家又接着去干活去了。过了一阵子或者几天，想回想一下当时会上到底是怎么说的或者讨论了什么方案，很可能就忘了。如果当时会上有专门做会议纪要的人，那还 OK，如果没有，那这么多人过了一段时间，具体会上说了那些有用的东西或者最后采取了什么方案具体怎么实施呢？很可能就是两个字：忘了。另外，有些人可能整个会上一句话也没说，完全感觉不到任何参与感，所以他也就越来越觉得这个会议没有什么意义，甚至可能就在会上就睡着了。 我想工作的各位难免会遇到这样的问题，或者甚至大家的会议现状就是这样子的。我之前也一直觉得，这种会议模式其实挺病态的，在时间这么宝贵的今天，到底有没有一个比较好的开会方式呢？ 今天听了这个分享之后，我确实被其中所讲的一个会议模式吸引到了。 怎么个方式呢？这里我就根据自己的理解大体概括一下。</p>
                  <h2 id="提前准备"><a href="#提前准备" class="headerlink" title="提前准备"></a>提前准备</h2>
                  <p>会议的组织者，在开会之前，根据会议的适用场景，列好这个会议想要讨论的内容，以文档的形式写下来。比如要开发一个软件，那么可能就列出来其中的交互或实现方案。如果是要讨论一个解决方案，那就把已经想到的解决方案写下来。 然后呢，很重要的，在开会之前，把文档都发给大家，比如附在邮件链接里面，大家都可以下载或者在线查看。这样可以让大家提前对会议的内容有所了解，如果是对会议毫不了解的人，也能对会议的主题有个整体的把握。 这样就不会出现这样的情况了： 咦，来了一个会啊，咋叫上我了？这个会到底干嘛的？写的这个主题到底啥意思？我去了能干啥？ 这就是其一，提前准备，让所有人都有所了解。 另外，每个人知道主题和讨论内容了，也就知道自己需要准备什么东西，哪些需要演示，哪些需要重点讲解。</p>
                  <h2 id="阅读及评论"><a href="#阅读及评论" class="headerlink" title="阅读及评论"></a>阅读及评论</h2>
                  <p>这个环节也很新颖和高效。 在会议开始的时候，前 15 分钟（时间视情况而定），没有任何人讲话！注意是没有任何人讲话！ 那么大家做什么？看文档！ 大家会在这前 15 分钟里面仔细阅读文档和思考。由于文档是共享的，可以在线协作编辑的，每个人都可以在上面写 Comments，比如提意见或者提想法。由于大家都登录了自己的账户，所以谁提的 Comments，所有人都一清二楚，而且可以实时看到。 就类似下图的这种感觉，见图右侧的 Comments。 <img src="https://qiniu.cuiqingcai.com/2020-02-05-151914.png" alt="image-20200205231912431"> 然后 15 分钟过后，大家会针对大家疑虑的点进行讨论。 注意，由于有了 Comments，大家就知道一共有多少需要讨论的点，这个会还剩余多久，每个 Comment 可以讨论多长时间，这样又避免了开会时间无节制的问题。 然后还有一个重要的点，就是每讨论完一个 Comment，就在对应的地方写上解决方案或者附上 Todo List，即具体的实施措施，做到当场讨论问题、当场提出解决方案、当场分配任务计划。 OK，然后整个会就很有目标地开完了，很有侧重点，而且大家都清除的就不需要讨论了。 另外还有一个优点就是，每个人都可以提 Comments，这样每个人都会感到极强的参与感，再也不会出现会上一言不发事不关己高高挂起的状态了。</p>
                  <h2 id="会后"><a href="#会后" class="headerlink" title="会后"></a>会后</h2>
                  <p>由于采取了前面的两步措施，所以会后也就更方便了。 整个会议的纪要在哪里？就是之前的文档里。 整个会议关键的点在哪里？都在 Comments 和对应的回复里。 整个会议讨论出了什么计划？当场也已经分配好了，大家会后直接根据会上分配的 Todo 去做就好了，分配的任务会自动加到每个人的 Todo List 里面。 过了几天，我想复盘整个会议或者回想下这个会议说了些啥，怎么办？直接打开会议文档就好了。 还需要专门写会议纪要的吗？会上大家共同协作会议文档，已经都写好了。 嗯，这就是整个会议的模式。 节省了多少时间或者避免了什么问题呢？我们数数吧。</p>
                  <ul>
                    <li>每个人在会前都可以对会议提前了解和准备，每个人的了解和准备都会更充分。</li>
                    <li>开会前半段每个人都会有单独的时间去理解和思考并做评论。</li>
                    <li>每个人都会有极强的参与感。</li>
                    <li>根据大家评论的数量来把握整个开会的节奏，减少了拖延的概率。</li>
                    <li>会上当场确定好解决方案和人员，自动同步到每个人的 Todo 列表。</li>
                    <li>会后随时查看，随时复盘。</li>
                    <li>文档记录，永不丢失遗忘。</li>
                  </ul>
                  <p>真的可以说，这个模式我觉得非常好，大幅提高了开会的效率，节省了时间。 这个模式我听 Talk 里面说已经在诸如头条的公司里面用了很久了，很多人都反馈很不错。飞书在这方面的支持已经做得挺好了，但可能很多公司不用飞书，不过现在还有很不错的在线协作文档，比如 石墨文档、Pages、Worktile、OneNote 等等也都可以成为候选方案，这个模式还是完全可以借鉴的。 以上仅是我的一点总结和思考，希望对大家有所启发，谢谢！</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-02-10 01:34:12" itemprop="dateCreated datePublished" datetime="2020-02-10T01:34:12+08:00">2020-02-10</time>
                </span>
                <span id="/8891.html" class="post-meta-item leancloud_visitors" data-flag-title="关于开会的一些思考" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>2.4k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8889.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 个人随笔 <i class="label-arrow"></i>
                  </a>
                  <a href="/8889.html" class="post-title-link" itemprop="url">2020 才过去了一个多月，世界都发生了些什么</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>有人说：2019 年可能是过去十年里最坏的一年，但可能是未来十年里最好的一年。 的确 2019 整个大环境确实比较差，很多人可能在 2020，这个新的一个十年的开端，许愿接下来的日子能慢慢好起来。但目前的状况，大家可能都看到了，新型冠状病毒的肆虐，让全国都变成了什么样子。在国内，新型冠状病毒相关的态势一直最近的头条，但可能大家并没有注意到，其实世界各地都似乎不怎么太平，如。而看看时间，2020 才过去了一个多月而已。</p>
                  <h2 id="中国"><a href="#中国" class="headerlink" title="中国"></a>中国</h2>
                  <p>不知道大家是不是和我一样，起床醒来的第一件事就是去查一查现在国内新型冠状病毒的确诊人数。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-131059.png" alt="image-20200209211057573"> 截止今天（2 月 9 日）晚上 9 点，全国确诊病例达到 37289 例，疑似病例 28942 例，每天还以好几千的数量增加，估计明天早上能突破 4 万。 全国 31 个省区均早已经启动了一级响应，这在过去是从未有过的，随着病毒的传播蔓延，事态的严重性远远超出人们最大胆的想象。现在国家正在举全国之力支援灾区，前线的医疗人员一直在全力以赴救治患者。全国各个地区也在采取可以说是历史上最严格的防疫措施，全国范围内的商场、餐厅、娱乐场所几乎都被关停，各家各户的居民也都在家隔离，企业也延迟复工或者到现在还有很多企业都没有正式复工，口罩物资都已经全网脱销，一罩难求。但没有办法，在这个非常时刻，只要我们每个人都尽上自己的所能，相信疫情肯定会慢慢控制下来，请相信我们的国家。 观察了几天，从数据上来看，可能有这么两个好消息： <img src="https://qiniu.cuiqingcai.com/2020-02-09-133307.png" alt="image-20200209213305882"></p>
                  <ul>
                    <li>一个是现在总的治愈人数已经超过死亡人数接近三倍，而且治愈人数的每日增长速度已经远超过死亡人数，比如今天的治愈人数就已经是死亡人数的大约 10 倍（891：90）。而且现在疫苗已经投入临床试验，相信接下来的治愈数据的增长液会越来越快。</li>
                    <li>新增的确诊和疑似人数出现缓和和下降迹象，至少从最近四天的数据上来看，能看出新增数量整体呈现下降的态势，今天刚刚也有新闻报道说全国除湖北外其他省份每日报告的确诊病例数从 2 月 3 日 890 例下降到 2 月 8 日的 509 例，下降幅度达到 42.8%，这表明各地联防联控机制以及严格管理等防控措施正在发挥作用。希望前几天数据最高的点，就是那个拐点吧。</li>
                  </ul>
                  <p>加油武汉，加油中国！💪</p>
                  <h2 id="美国"><a href="#美国" class="headerlink" title="美国"></a>美国</h2>
                  <p>可能我们大多数人关注更多的是国内的新型冠状病毒。但现在，美国其实也不太平。美国正在遭受近 10 年来最严重的流感疫情。 看图，这是美国疾病控制与预防中心网站上的流感活动地图，现在是 2 月 9 日周日，数据的统计是每周一次更新，当前更新到 2 月 1 日，数据来源链接见文末。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-135555.png" alt="image-20200209215553293"> CDC 称，在其流感活动地图上，美国大部分地区为深红色，表明「流感样疾病」活动水平达到最高级。由于美国的流感季还要持续一段时间，因此这一数据可能会继续上升。从地区上看，目前全美 50 个州里，有 48 个州出现流感疫情，其中 32 个州流感活动水平维持高位，人口稠密的纽约、华盛顿和加州无一幸免，纷纷中招。此次流感季将是10年来美国最严重的流感季之一，2019 - 2020 流感季中，美国 1900 万人感染流感，至少 1 万人死亡，包括 68 名儿童。预计流感季将持续至 5 月，而 2 月是高峰期。 我有几个亲戚朋友现在就住在美国，因为流感的问题，每天他们也是和我们一样，待在家里不敢出门，同样体会出来了 ”隔离“ 的滋味。</p>
                  <h2 id="澳洲"><a href="#澳洲" class="headerlink" title="澳洲"></a>澳洲</h2>
                  <p>澳洲最近同样也不太平。 之前大家可能有听说澳洲大火的消息，其实关于澳洲大火的报道，去年 10 月份开始就有了，本来大家以为只是一场普通的森林火灾，扑灭就完事了。我看最近有报道澳洲大火的消息的时候，很多人在底下评论，大火一直在烧啊？没错，这大火一直烧，四个月了，烧的时候澳洲的卫星地图状况就是这个样子： <img src="https://qiniu.cuiqingcai.com/2020-02-09-141721.jpg" alt="img"> 这场火，不仅造成人命伤亡及经济损失，也对自然生态带来毁灭性破坏，使数亿动物遭遇灭顶之灾。已有近 5 亿只动物死于澳洲山火，并据相关报道显示考拉或将功能性灭绝。悉尼大学发布报告，全国约有 10 亿动物被大火波及，其中仅在澳大利亚袋鼠岛的大火中就有超 2 万只考拉死亡，考拉将被列为濒危动物。澳大利亚全境被烧毁的森林面积约 1120万 公顷，而去年震惊世界的亚马孙雨林大火烧毁森林面积才约 180 万公顷。此外，有 1400多公里的海岸线都在燃烧，相当从东北烧到了江浙沪。 慢慢地，前几天，山火逐渐逼近了堪培拉。1 月 31 日，因为山火的步步紧逼，澳大利亚政府宣布堪培拉进入紧急状态，这是自 2003 年山火危机之后 17 年以来，堪培拉首次进入紧急状态。2 月 2 日整天，堪培拉均处于高度戒备状态。堪培拉和周边地区的气温一度超过 42 摄氏度。 当时堪培拉就是这么一个状态： <img src="https://qiniu.cuiqingcai.com/2020-02-09-142308.png" alt="image-20200209222306881"> 其他的地区山火基本是这么一个状态： <img src="https://qiniu.cuiqingcai.com/2020-02-09-142602.jpg" alt="img"> 以及那些被烧死的无辜的动物们： <img src="https://qiniu.cuiqingcai.com/2020-02-09-142729.jpg" alt="img"> 澳大利亚的这场大火也对全球造成了影响。欧洲哥白尼大气监测服务发表数据：澳大利亚已经向大气排放约 4 亿吨二氧化碳，这个数据比全球 116 个二氧化碳排放量最低国家年排放量总和还要高。 不过，就在最近几天，现在澳洲的山火由于一场暴雨的到来，很多地区的火已经灭了。但，这还没完，由于雨过大，洪水又来了。 据《每日邮报》2 月 9 日报道，近日，澳大利亚遭遇了十年来最大降雨，东海岸被大规模破坏，悉尼正在全力应对洪水爆发，火车站都变成了游泳池。澳大利亚气象局预计，在新南威尔士州北部的一些气象站在 48 小时内录得超过 300 毫米的降雨后，降雨将持续到周日。 据报道，周六，在新南威尔士州北部，67 岁的吉尔·萨瑟兰和她 30 岁的侄女汉娜驱车前往位于新南威尔士州北部河流地区的宁宾，途中他们穿过了一条被洪水淹没的道路。然而，他们很快就失去了对汽车的控制，车子灌满了水，沉了下去，完全从视线中消失了。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-143813.png" alt="image-20200209223811908"> 但好在，大火已经基本停了，但愿这次洪水也能尽快过去，祝好！</p>
                  <h2 id="巴西"><a href="#巴西" class="headerlink" title="巴西"></a>巴西</h2>
                  <p>当地时间 1 月 26 日，巴西东南部因受强风暴雨影响，正在遭遇百年不遇的泥石流灾害。 暴雨接连两日肆虐米纳斯吉拉斯州，导致多处房屋倒塌，道路摧毁，很多生命瞬间流逝。到目前为主，此次灾情至少已造成 46 人死亡，超过 25000 人流离失所。巴西国家气象研究所表示，这是自 110 年前开始有纪录以来，本地区降下的最猛烈暴雨。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-144450.png" alt="image-20200209224448748"> 军队现已对山区的村镇与交通设施展开全力救助，巴西政府也声称将努力建立一个全国性的灾害预防和早期预警系统。</p>
                  <h2 id="波兰、丹麦"><a href="#波兰、丹麦" class="headerlink" title="波兰、丹麦"></a>波兰、丹麦</h2>
                  <p>1 月 23 日，波兰海关总署发布消息。1 月 7 日，波兰官方向世界动物卫生组织通报，2019 年 12 月 31 日至 2020 年 1 月 4 日，该国卢布林省和大波兰省发生 8 起 H5N8 亚型高致病性禽流感。波兰是欧洲最大的家禽生产国，自2017 年以来从未爆发过禽流感。 1 月 30 日，丹麦环境和食品部向 OIE 紧急报告称，丹麦发生一起 H5N1 型低致病性禽流感疫情。本次疫情于 1 月 29 日得到确认，此次疫情可能导致多达 4 万只禽类被宰杀，方圆 3 公里多达 35 万只家禽受到威胁。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-145504.jpg" alt="img"></p>
                  <h2 id="法国、西班牙"><a href="#法国、西班牙" class="headerlink" title="法国、西班牙"></a>法国、西班牙</h2>
                  <p>1 月 21日，西班牙东北部地区遭强暴风和大雪袭击，供电中断，几十万人无电可用，暴风和大雪至少已造成 4 人死亡。阿联酋《宣言报》22 日报道称，由于暴风和降大雪，能见度很低，气温急速下降，达到结冰的程度，地中海沿岸海浪急剧上升。 西班牙紧急部门的报告表示，虽然与法国之间的供给线得到了修复，但因为积雪覆盖了2600多公里的道路，交通受阻，供电难以解决，位于东北部的吉罗纳省 22 万居民仍然没有电可用。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-145710.png" alt="image-20200209225709188"></p>
                  <h2 id="土耳其"><a href="#土耳其" class="headerlink" title="土耳其"></a>土耳其</h2>
                  <p>土耳其内政部 1 月 25 日消息称，24 日晚发生在该国东部埃拉泽省的 6.8 级地震已经造成 22 人死亡，超过 1000 人受伤。 据土耳其阿纳多卢通讯社报道，土耳其内政部部长索伊卢 25 日在新闻发布会上表示，埃拉泽省在地震中的死亡人数为 18 人，其邻省马拉提亚省死亡人数为 4 人。政府派出的救援队已从倒塌的房屋和建筑物的废墟中救出 39 人。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-150519.png" alt="image-20200209230517249"></p>
                  <h2 id="利比亚"><a href="#利比亚" class="headerlink" title="利比亚"></a>利比亚</h2>
                  <p>根据俄罗斯卫星通讯社开罗 1 月 14 日的报道，利比亚冲突各方停火问题谈判 13 日在莫斯科举行，俄土两国代表参加了谈判。 然而利比亚战火和谈失败，双方陷入了僵局。土耳其出兵面临全面内战永久分裂风险。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-150608.png" alt="image-20200209230607701"></p>
                  <h2 id="苏丹"><a href="#苏丹" class="headerlink" title="苏丹"></a>苏丹</h2>
                  <p>1 月 14 日下午，苏丹发生政变，安全部门和军方在首都喀土穆机场附近发生激烈对峙，并伴有阵阵枪声。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-150849.png" alt="image-20200209230847784"> 军方戒严了城区主要街道，喀土穆国际机场已关闭。中国驻苏丹使馆发出安全警告，提醒中国公民不要靠近机场区域。</p>
                  <h2 id="也门"><a href="#也门" class="headerlink" title="也门"></a>也门</h2>
                  <p>1 月 19 日，也门西部一个军事训练营遭遇袭击，造成数十名也门政府军士兵死亡，至少 100 人受伤，胡塞武装在马里布市发动了大规模伤亡袭击，命令也门军队必须保持高度戒备，做好战斗准备。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-150959.png" alt="image-20200209230958809"></p>
                  <h2 id="印尼"><a href="#印尼" class="headerlink" title="印尼"></a>印尼</h2>
                  <p>印尼国家抗灾署 6 号通报，首都雅加达和周边地区日前遭遇的洪灾，已造成 67 人死亡。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-151243.jpg" alt="img"> 印尼国家抗灾署发言人阿古斯在一份声明中说，截至当地时间 6 日下午，灾害还造成 1 人失踪，另有 3.6 万人居住在临时避难所。由于强降雨引发洪灾、山体滑坡等灾害，雅加达周边的 12 个县市已陆续宣布进入为期一到两周的紧急状态，以便救援和物资运输工作展开。</p>
                  <h2 id="东非"><a href="#东非" class="headerlink" title="东非"></a>东非</h2>
                  <p>东非近期爆发蝗灾，这被称为 70 年来最严重的沙漠蝗灾，其中肯尼亚受灾最为严重，数亿只蝗虫在肯尼亚境内肆虐。 据联合国估计此次蝗虫数量达 3600 亿只，且数量可能在数月后暴增 500 倍。非常令人惊恐的是沙漠蝗群一天内可以吃掉能养活 2500 人的粮食，这也令当地出现了严重的粮食危机。蝗群密度一般达到每平方公里 1.5 亿只，蝗群一天可以随风飞行 100 至 150 公里，破坏范围及力量惊人。 <img src="https://qiniu.cuiqingcai.com/2020-02-09-152055.png" alt="image-20200209232054576"> 联合国粮食及农业组织已经发出警告，蝗灾将会造成近年来罕见的粮食危机，1900 万人将面临危及生命的饥荒。 这个确实非常严重的，一些更详细的报道大家可以了解澎湃新闻报道的视频：<a href="https://www.thepaper.cn/newsDetail_forward_5758503，真的难以想象" target="_blank" rel="noopener">https://www.thepaper.cn/newsDetail_forward_5758503，真的难以想象</a>。</p>
                  <h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2>
                  <p>总的来说，全世界的 2020 开局的确可以说是不怎么太平。仔细想想，这些现象的发生有天灾，有人祸。除了一些政治上的问题，这些灾难其实来源于人们对大自然的过渡攫取，或者说是对野生动物的滥杀和捕食。 多难兴邦，天灾无情人有情。在大自然的面前，我们显得非常渺小，在灾难的面前，我们都是受害者。这时候就需要我们团结一心，站在一个战线上去把这些困难渡过去，而不是在这个节骨眼上发国难财，造谣传谣。像我们之前非典、汶川地震一样，众志成城，再难我们也能挺过去的。 相信我们的国家，相信世界的人们。 加油武汉，加油中国，加油全世界！</p>
                  <h2 id="参考来源"><a href="#参考来源" class="headerlink" title="参考来源"></a>参考来源</h2>
                  <ul>
                    <li>本文选题和主要参考来源：2020年只过去了一个月，全世界正在发生什么？<a href="https://mp.weixin.qq.com/s/V1iioQsuYITpFfRijZkQHw" target="_blank" rel="noopener">https://mp.weixin.qq.com/s/V1iioQsuYITpFfRijZkQHw</a></li>
                    <li>卫健委：全国除湖北外其他省份确诊病例下降超40% <a href="http://news.sina.com.cn/c/2020-02-09/doc-iimxxstf0048640.shtml" target="_blank" rel="noopener">http://news.sina.com.cn/c/2020-02-09/doc-iimxxstf0048640.shtml</a></li>
                    <li>美国4个月近2000万人感染流感 今年或是10年来最严重 <a href="https://www.codingsky.com/news/2020-02-08/11567.html" target="_blank" rel="noopener">https://www.codingsky.com/news/2020-02-08/11567.html</a></li>
                    <li>Weekly U.S. Influenza Surveillance Report <a href="https://www.cdc.gov/flu/weekly/index.htm#ILIActivityMap" target="_blank" rel="noopener">https://www.cdc.gov/flu/weekly/index.htm#ILIActivityMap</a></li>
                    <li>澳洲遭十年最大暴雨，悉尼全力应对洪水爆发 <a href="https://new.qq.com/rain/a/20200209A06URA" target="_blank" rel="noopener">https://new.qq.com/rain/a/20200209A06URA</a></li>
                    <li>巴西遭创纪录暴雨肆虐 泥石流冲毁房屋 <a href="http://www.chinanews.com/m/tp/hd/2020/0126/134972.shtml" target="_blank" rel="noopener">http://www.chinanews.com/m/tp/hd/2020/0126/134972.shtml</a></li>
                    <li>也门政府军遭胡塞武装导弹袭击至少40人死亡 <a href="http://www.xinhuanet.com/mil/2020-01/19/c_1210444134.htm" target="_blank" rel="noopener">http://www.xinhuanet.com/mil/2020-01/19/c_1210444134.htm</a></li>
                    <li>苏丹安全部门和军方在喀土穆机场附近激烈对峙 <a href="https://news.sina.cn/gj/2020-01-15/detail-iihnzhha2484546.d.html" target="_blank" rel="noopener">https://news.sina.cn/gj/2020-01-15/detail-iihnzhha2484546.d.html</a></li>
                    <li>67人死亡 1人失踪 印尼洪水灾情严重 <a href="http://news.cri.cn/20200107/235b849d-523f-b512-c6d4-4feacd8347f4.html" target="_blank" rel="noopener">http://news.cri.cn/20200107/235b849d-523f-b512-c6d4-4feacd8347f4.html</a></li>
                    <li>东非遭70年来最严重蝗灾，情况或将恶化 <a href="https://www.thepaper.cn/newsDetail_forward_5758503" target="_blank" rel="noopener">https://www.thepaper.cn/newsDetail_forward_5758503</a></li>
                  </ul>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-02-10 01:33:41" itemprop="dateCreated datePublished" datetime="2020-02-10T01:33:41+08:00">2020-02-10</time>
                </span>
                <span id="/8889.html" class="post-meta-item leancloud_visitors" data-flag-title="2020 才过去了一个多月，世界都发生了些什么" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>4.9k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>4 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8811.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/8811.html" class="post-title-link" itemprop="url">Kubernetes 批量部署 Splash 服务</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>做爬虫的小伙伴可能听说过 Splash，它可以提供动态页面渲染服务，如果我们要爬的某些页面是 JavaScript 渲染而成的，此时我们直接用 requests 或 Scrapy 来爬是没法直接爬到的，此时我们可以借助于 Splash 来帮我们把 JavaScript 渲染后的真实页面结果拿下来。 不过 Splash 在大批量爬虫使用的时候坑不少，Splash 可能用着用着可能就内存炸了，如果只是单纯启 Docker 服务又不好 Scale，另外也不方便当前服务的使用状态，比如内存占用、CPU 消耗等等。 最近把 Splash 迁移到了 Kubernetes 上面，正好上面的问题就一带解决了。 我们既可以方便地扩容，又可以设置超额重启，又可以方便地观察到当前服务使用情况。 下面简单记录一下我把 Splash 迁移到 Kubernetes 上面的过程，真的迁移过来之后省了很多麻烦，推荐大家也可以试试。 好，下面正式开始介绍。</p>
                  <h2 id="必备条件"><a href="#必备条件" class="headerlink" title="必备条件"></a>必备条件</h2>
                  <p>首先，我们需要有一个 Kubernetes 集群，可以自己搭建，也可以使用 Minikube 或者用阿里云、腾讯云、Azure 等服务商直接提供的 Kubernetes 服务。 另外我们需要能使用 <code>kubectl</code> 连接和控制当前的集群，同时需要安装好 <code>helm</code> 并配置好 stable 版本的 Charts，在这里我使用的是 Helm 2.x。 在这里列一些参考资料：</p>
                  <ul>
                    <li>搭建 Kubernetes 集群：<a href="https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/" target="_blank" rel="noopener">https://kubernetes.io/docs/setup/production-environment/tools/kubeadm/create-cluster-kubeadm/</a></li>
                    <li>Minikube：<a href="https://kubernetes.io/zh/docs/setup/learning-environment/minikube/" target="_blank" rel="noopener">https://kubernetes.io/zh/docs/setup/learning-environment/minikube/</a></li>
                    <li>Helm V2 安装和使用：<a href="https://v2.helm.sh/docs/" target="_blank" rel="noopener">https://v2.helm.sh/docs/</a></li>
                    <li>Charts：<a href="https://github.com/helm/charts" target="_blank" rel="noopener">https://github.com/helm/charts</a></li>
                  </ul>
                  <p>上面的内容准备就绪之后，我们就可以开始 Kubernetes 搭建 Splash 服务的流程了。</p>
                  <h2 id="整体流程"><a href="#整体流程" class="headerlink" title="整体流程"></a>整体流程</h2>
                  <ul>
                    <li>创建 NameSpace</li>
                    <li>创建 Service</li>
                    <li>创建 Deployment</li>
                    <li>安装 Ingress Controller</li>
                    <li>配置域名解析</li>
                    <li>配置 Authentication</li>
                    <li>配置 HTTPS</li>
                  </ul>
                  <p>上面就是本节所要介绍的基本内容，下面我们开始吧。</p>
                  <h2 id="创建-NameSpace"><a href="#创建-NameSpace" class="headerlink" title="创建 NameSpace"></a>创建 NameSpace</h2>
                  <p>首先我们将 Splash 安装在一个独立的 Namespace 下面，名字就叫做 splash 吧。 yaml 内容如下：</p>
                  <figure class="highlight dts">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="symbol">apiVersion:</span> v1</span><br><span class="line"><span class="symbol">kind:</span> Namespace</span><br><span class="line"><span class="symbol">metadata:</span></span><br><span class="line"><span class="symbol">  name:</span> splash</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样就声明了一个 NameSpace，名字叫做 splash。</p>
                  <h2 id="创建-Service"><a href="#创建-Service" class="headerlink" title="创建 Service"></a>创建 Service</h2>
                  <p>Service 的创建也很简单，我们注意声明好 namespace 和端口等信息即可：</p>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">v1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Service</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">labels:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">splash</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">splash</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">splash</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">ports:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">name:</span> <span class="string">"8050"</span></span><br><span class="line">      <span class="attr">port:</span> <span class="number">8050</span></span><br><span class="line">      <span class="attr">targetPort:</span> <span class="number">8050</span></span><br><span class="line">  <span class="attr">selector:</span></span><br><span class="line">    <span class="attr">app:</span> <span class="string">splash</span></span><br><span class="line"><span class="attr">status:</span></span><br><span class="line">  <span class="attr">loadBalancer:</span> <span class="string">&#123;&#125;</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里选择了端口号 port 8050，即服务运行的端口为 8050。targetPort 也是 8050，这个代表 Pod 里面容器的运行端口。另外声明了 labels 和 selector 的内容，大家可以稍作了解。</p>
                  <h2 id="创建-Deployment"><a href="#创建-Deployment" class="headerlink" title="创建 Deployment"></a>创建 Deployment</h2>
                  <p>接下来，就是最关键的了，我们使用 scrapinghub/splash 这个 Docker 镜像来创建一个 Deployment，yaml 文件如下：</p>
                  <figure class="highlight less">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">apiVersion</span>: apps/v1</span><br><span class="line"><span class="attribute">kind</span>: Deployment</span><br><span class="line"><span class="attribute">metadata</span>:</span><br><span class="line">  <span class="attribute">labels</span>:</span><br><span class="line">    <span class="attribute">app</span>: splash</span><br><span class="line">  <span class="attribute">name</span>: splash</span><br><span class="line">  <span class="attribute">namespace</span>: splash</span><br><span class="line"><span class="attribute">spec</span>:</span><br><span class="line">  <span class="attribute">replicas</span>: <span class="number">3</span></span><br><span class="line">  <span class="attribute">selector</span>:</span><br><span class="line">    <span class="attribute">matchLabels</span>:</span><br><span class="line">      <span class="attribute">app</span>: splash</span><br><span class="line">  <span class="attribute">revisionHistoryLimit</span>: <span class="number">1</span></span><br><span class="line">  <span class="attribute">strategy</span>: &#123;&#125;</span><br><span class="line">  <span class="attribute">template</span>:</span><br><span class="line">    <span class="attribute">metadata</span>:</span><br><span class="line">      <span class="attribute">labels</span>:</span><br><span class="line">        <span class="attribute">app</span>: splash</span><br><span class="line">    <span class="attribute">spec</span>:</span><br><span class="line">      <span class="attribute">containers</span>:</span><br><span class="line">        - <span class="attribute">image</span>: scrapinghub/splash</span><br><span class="line">          <span class="attribute">name</span>: splash</span><br><span class="line">          <span class="attribute">ports</span>:</span><br><span class="line">            - <span class="attribute">containerPort</span>: <span class="number">8050</span></span><br><span class="line">          <span class="attribute">resources</span>:</span><br><span class="line">            <span class="attribute">requests</span>:</span><br><span class="line">              <span class="attribute">memory</span>: <span class="string">"1Gi"</span></span><br><span class="line">              <span class="attribute">cpu</span>: <span class="string">"1"</span></span><br><span class="line">            <span class="attribute">limits</span>:</span><br><span class="line">              <span class="attribute">memory</span>: <span class="string">"4Gi"</span></span><br><span class="line">              <span class="attribute">cpu</span>: <span class="string">"4"</span></span><br><span class="line">      <span class="attribute">restartPolicy</span>: Always</span><br><span class="line"><span class="attribute">status</span>: &#123;&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里也有几个比较关键的点：</p>
                  <ul>
                    <li><code>metadata.labels</code>：这里需要和 Service 里面的 selector 对应起来。</li>
                    <li><code>spec.template.spec.containers[]</code>：这里声明 splash 的镜像，用的是 latest 镜像 scrapinghub/splash；端口地址用的 8050；restartPolicy 使用的是 Always，这样 Splash 如果崩溃了会自动重启；resources 设置了使用的内存和 CPU 的请求和限制值，这里大家可以根据机器和爬取需求自行修改。</li>
                    <li><code>spec.replicas</code>：运行的实例个数，这里设置为了 3，这样就会启动 3 个 Splash ，Service 会对其负载均衡。</li>
                  </ul>
                  <p>好了，写了上面三个 yaml，我们可以将其合并到一个 yaml 文件里面，如 <code>deployment.yml</code>，然后执行：</p>
                  <figure class="highlight coq">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">kubectl <span class="built_in">apply</span> -f deployment.yml</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就可以观察到 NameSpace、Service、Deployment 都创建成功了，Pod 随之也创建成功了。 <img src="https://qiniu.cuiqingcai.com/2020-01-29-131028.png" alt="image-20200129211026981"></p>
                  <h2 id="安装-Ingress-Controller"><a href="#安装-Ingress-Controller" class="headerlink" title="安装 Ingress Controller"></a>安装 Ingress Controller</h2>
                  <p>接下来我们想要配置一个域名解析，并配置好 HTTPS。 首先我们需要安装 Ingress，这里我们使用 Helm 2.x 安装，使用的 Charts 为：<a href="https://github.com/helm/charts/tree/master/stable/nginx-ingress" target="_blank" rel="noopener">https://github.com/helm/charts/tree/master/stable/nginx-ingress</a>。 这里我们稍作修改，指定 NameSpace 和镜像即可：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">helm install --name ingress-splash --namespace splash --<span class="builtin-name">set</span> defaultBackend.image.<span class="attribute">repository</span>=mirrorgooglecontainers/defaultbackend-amd64 stable/nginx-ingress</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里我们将镜像修改了一下，避免国内 Kubernetes 无法拉取镜像的问题。 OK，安装好了之后，可以看到 Ingress Controller 就安装成功了。</p>
                  <h2 id="域名解析"><a href="#域名解析" class="headerlink" title="域名解析"></a>域名解析</h2>
                  <p>域名解析就好配置了，直接将域名配置到 Ingress Controller Service 的 External IP 上面即可。 <img src="https://qiniu.cuiqingcai.com/2020-01-29-131156.png" alt="image-20200129211154610"></p>
                  <h2 id="配置-Authentication"><a href="#配置-Authentication" class="headerlink" title="配置 Authentication"></a>配置 Authentication</h2>
                  <p>Splash 部署完了之后，默认是没有 Authentication 的，如果直接暴露在公网中，是可以被他人直接使用的。 所以我们需要对其配置 Authentication，并配置 Ingress 域名解析。 这里 Authentication 我们使用 HTTP Basic Auth 就好了，要配置这个，我们需要先新建一个 Secret。 那么 Secret 怎么创建呢，我们先用 htpasswd 生成一个秘钥文件，用户名为 splash：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">htpasswd -c auth splash</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>执行完了之后本地会生成一个 auth 文件，我们用这个 auth 文件创建一个 Secret 即可：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">kubectl create<span class="built_in"> secret </span>generic basic-auth <span class="attribute">--from-file</span>=auth --namespace splash</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样 Secret 就创建好啦，用户名就是 splash，密码就是刚才创建秘钥文件时输入的密码。 上面更详细的介绍参见：<a href="https://kubernetes.github.io/ingress-nginx/examples/auth/basic/" target="_blank" rel="noopener">https://kubernetes.github.io/ingress-nginx/examples/auth/basic/</a> 好，然后我们创建 Ingress。 新建 ingress.yml 文件内容如下：</p>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">networking.k8s.io/v1beta1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Ingress</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">ingress-splash</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">splash</span></span><br><span class="line">  <span class="attr">annotations:</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/auth-type:</span> <span class="string">basic</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/auth-secret:</span> <span class="string">basic-auth</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/auth-realm:</span> <span class="string">'Authentication Required'</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">host:</span> <span class="string">&lt;domain&gt;</span></span><br><span class="line">      <span class="attr">http:</span></span><br><span class="line">        <span class="attr">paths:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">backend:</span></span><br><span class="line">              <span class="attr">serviceName:</span> <span class="string">splash</span></span><br><span class="line">              <span class="attr">servicePort:</span> <span class="number">8050</span></span><br><span class="line">            <span class="attr">path:</span> <span class="string">/</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里 <code>metadata.annotations</code> 里面声明了三个选项，就是设定 HTTP Basic Auth 的。 注意这里 <code>spec.rules[].host</code> 的内容换成自己的域名。 好，然后执行即可：</p>
                  <figure class="highlight coq">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">kubectl <span class="built_in">apply</span> -f ingress.yml</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>OK，这样 Ingress 也创建好啦。 现在我们只需要访问 <code>http://&lt;domain&gt;</code> 即可访问到 Splash 服务啦。 初次访问需要输入用户名和密码，如图所示。 <img src="https://qiniu.cuiqingcai.com/2020-01-29-133031.png" alt="image-20200129213029854"> 登录完成之后就可以看到 Splash 的界面了，如图所示。 <img src="https://qiniu.cuiqingcai.com/2020-01-29-132037.png" alt="image-20200129212035193"></p>
                  <h2 id="HTTPS"><a href="#HTTPS" class="headerlink" title="HTTPS"></a>HTTPS</h2>
                  <p>这时候整个 Endpoint 是 HTTP 协议，会被提示不安全，如果我们想要配置 HTTPS，还需要申请一个证书。 证书可以到阿里云、腾讯云等等服务商申请即可。 申请完了，我们可以得到 crt 和 key 两个文件。 接下来我们首先需要配置一个 tls 类型的 Secret，命令如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">kubectl create<span class="built_in"> secret </span>tls tls-splash -n splash --cert &lt;cert_name&gt;.crt --key &lt;cert_name&gt;.key</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里 <code>&lt;cert_name&gt;</code> 替换成你申请的证书文件名即可。 这样我们就创建了一个名字为 tls-splash 的 Secret，下面我们开始使用。 修改 ingress.yml 文件如下：</p>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">apiVersion:</span> <span class="string">networking.k8s.io/v1beta1</span></span><br><span class="line"><span class="attr">kind:</span> <span class="string">Ingress</span></span><br><span class="line"><span class="attr">metadata:</span></span><br><span class="line">  <span class="attr">name:</span> <span class="string">ingress-splash</span></span><br><span class="line">  <span class="attr">namespace:</span> <span class="string">splash</span></span><br><span class="line">  <span class="attr">annotations:</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/auth-type:</span> <span class="string">basic</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/auth-secret:</span> <span class="string">basic-auth</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/auth-realm:</span> <span class="string">'Authentication Required'</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/ssl-redirect:</span> <span class="string">"true"</span></span><br><span class="line">    <span class="attr">nginx.ingress.kubernetes.io/rewrite-target:</span> <span class="string">/</span></span><br><span class="line"><span class="attr">spec:</span></span><br><span class="line">  <span class="attr">tls:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">hosts:</span></span><br><span class="line">        <span class="bullet">-</span> <span class="string">&lt;domain&gt;</span></span><br><span class="line">      <span class="attr">secretName:</span> <span class="string">tls-splash</span></span><br><span class="line">  <span class="attr">rules:</span></span><br><span class="line">    <span class="bullet">-</span> <span class="attr">host:</span> <span class="string">&lt;domain&gt;</span></span><br><span class="line">      <span class="attr">http:</span></span><br><span class="line">        <span class="attr">paths:</span></span><br><span class="line">          <span class="bullet">-</span> <span class="attr">backend:</span></span><br><span class="line">              <span class="attr">serviceName:</span> <span class="string">splash</span></span><br><span class="line">              <span class="attr">servicePort:</span> <span class="number">8050</span></span><br><span class="line">            <span class="attr">path:</span> <span class="string">/</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里主要修改的点有两个，一个是增加了 ssl 重定向，这样如果我们以 HTTP 访问过去，就会被跳转到 HTTPS 的地址。另外就是 <code>spec.tls</code> 字段了，这里声明 hosts 和 secretName 即可。 OK，重新应用：</p>
                  <figure class="highlight coq">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">kubectl <span class="built_in">apply</span> -f ingress.yml</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>大功告成，现在我们就可以使用 <code>https://&lt;domain&gt;</code> 来访问我们的 Splash 服务了。</p>
                  <h2 id="测试"><a href="#测试" class="headerlink" title="测试"></a>测试</h2>
                  <p>最后，输入个网址测试下吧，如百度，渲染成功，如图所示。 <img src="https://qiniu.cuiqingcai.com/2020-01-29-133132.png" alt="image-20200129213130109"> 以上，便是 Kubernetes 搭建 Splash 的方法。 希望对大家有帮助。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-01-29 23:54:06" itemprop="dateCreated datePublished" datetime="2020-01-29T23:54:06+08:00">2020-01-29</time>
                </span>
                <span id="/8811.html" class="post-meta-item leancloud_visitors" data-flag-title="Kubernetes 批量部署 Splash 服务" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>5.1k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>5 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8808.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/8808.html" class="post-title-link" itemprop="url">2019 年终总结：新生活、新探索</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>2020 年的新年过去了，去年也是在春节期间写的年终总结，今年也是时候再总结和反思一下我的 2019 年了。 总的来说，2019 我给自己的一句话总结为：新生活、新探索。 今年是我从学生时代正式迈入职场的第一年，也是体验了新的生活环境的第一年，没有预想到的变化有很多，接触的新的挑战也很多。这一年，我也在努力调整自己，去适应新的工作环境和生活节奏。但今年自己达成的面上的成就不算多，更多的时间在于学习、自我适应和调整，同时一年里我也有了一些新的感悟。 所以，在此把我这一年的变化、思考和新一年的目标做一下总结，希望来年可以继续加油。</p>
                  <h2 id="变化"><a href="#变化" class="headerlink" title="变化"></a>变化</h2>
                  <h3 id="第一次正式工作"><a href="#第一次正式工作" class="headerlink" title="第一次正式工作"></a>第一次正式工作</h3>
                  <p>2019 年 1 月份，我硕士毕业了，接下来就正式开始工作了。2019 年 3 月 1 日，我正式入职微软，在微软小冰部门，我换了一个新的小组，有了一群 Nice 的新同事。 我的工作的内容属于 AI Creation，不过偏全栈一点，会涉及到自然语言处理、图像处理、前端、后端等各个方面的技术。正式入职之后，我负责小组的这一个全新的探索方向。 怎么说呢？刚开始我接手这个项目的时候，基本上整个项目还是在实验阶段，我要做的就是把整个项目规范化、搭建一套完整的 End2End 的 Pipeline、检测模型、管理平台和服务器。我之前在实习阶段的时候没有接触过线上代码，不了解现在代码的一些逻辑和架构。所以刚开始的时候面对一些杂乱的实验数据、设计草稿、线上代码，面对这些一些需求，一段时间内真的是感觉不知道怎么办才好。那会儿我记得一直在梳理思路，在一点点 Debug 现有的代码的一些逻辑，然后和我的 Manager 和同事一起讨论实现的思路。 慢慢地，我逐渐也清楚了现有的代码和架构，知道了我可以具体怎么实现，期间我把自己的一些设计思路改了好几次，比如我记得有好几个 Pipeline 拆了又拆，有一些设计 Schema 改了好几次，最后慢慢稳定下来了，渐渐地，我把一些模型、Pipeline、管理平台、服务器慢慢搭建好了，现在我做的一些东西也上线并正式投入使用了。回想起来，刚开始真的挺难的，不过现在做成了，还是很有成就感的。 在这里真的要好好感谢我的 Manager 和同事们，他们对我的帮助很大。在讨论项目实现的过程中，我收获了很多新的想法。我承认我自己这个人并不是特别喜欢与人合作，倾向于坚持自己的想法，倾向于自己去把一件事去做完，所以我最开始可能更偏己见一点。但在讨论整个项目的过程中，我学到了，有些思路原来还可以这么想，原来还可以这么实现。真的，有些设计思路和想法我确实一开始没有想到，但经过讨论之后，确实学习到了很多技巧和方法，慢慢地，我的一些编程的思想也有了变化，我会有自己的想法，同时也倾向于把我的一些思路说给别人，看看别人怎么想的，在大多数情况下，我会想到更优的解决思路，即使没有，我也学到了一些新的思考方法或者知识点。另外在这期间，我的小组队伍也壮大了起来，我自己也作为小组长（算是）指导了几位新同事一起参与整个项目，在这期间我也悟到了一些合作或指导的一些经验，大家都非常给力，目前来说还是很不错的。 要说这一年的工作压力和强度的话，整个走下来其实还是不小的。在前期阶段，其实更多来自于项目本身的压力，因为有太多的东西需要做，同时需要学习和了解的新东西也有很多，当时也没有找到适合自己的工作节奏，算是“摸爬滚打”了好一阵。在后期阶段，也有一些新的挑战，比如要去思考哪些方向是对的，怎样和同事更好地协作一起优化一些功能点，当然也一直有新的技术需要学习。整体感觉上来，比我的实习期的工作强度大了非常非常多，可以说和实习期的工作强度是没法比的。 现在想想，实习的时候，我差不多半年时间写完了一本爬虫书，晚上还能和我的小伙伴们聚众开黑王者荣耀。现在？功能实现了吗？Bug 修完了吗？好了，滚去撸代码了。写书？开黑？一天，一晃就这么过去了。</p>
                  <h3 id="公众号"><a href="#公众号" class="headerlink" title="公众号"></a>公众号</h3>
                  <p>正式工作之后，发现自己打理公众号的时间比我预想中的要紧张许多，没错就是这个「进击的Coder」。 对于公众号的运营，我也慢慢地佛系了，在 2019 快年底的时候，我把我的公众号转给我的女朋友小马来运营了，不得不说她运营得非常好，会找一些很不错的技术文章帮我排版，帮我发布，我有原创文章我也会交给她帮我发一下，的确减轻了我不少的压力。她也有很多想法，写了一些原创，也会策划一些活动，后来，不少公众号的粉丝居然渐渐转成她的粉丝了？！我回来后，你们还认识我吗？ 说一下数据吧，写文章时，公众号粉丝数量为 <strong>57420</strong>，常读用户是 <strong>9857</strong>，平均阅读 <strong>3000</strong> 左右，算是技术号中比较普通的水平了，常读用户比例也不怎么高，而且对于我 2019 年的 Flag 10 万，还有挺大的差距，这个后文会详细说。 说说公众号这件事吧，为什么我这次单独把「常读用户」数据放出来了呢？因为现在其实粉丝数并不是那么重要的，常读用户才是更重要的。公众号在 2019 年做了一次大的改动，「信息流」是其一，「在看」是其二，另一个大的改动就是有了「常读的订阅号」这个功能，大家应该也都注意到了，它会出现在最上端，几个圆形的公众号头像，如果公众号有了消息，它会有一个绿色的小点，大多数情况下，然后我们就会点开看了，可以说对阅读量的帮助是很大的。对于信息流来说，如果我们发文的时机把握不好的话，很可能文章就会被冲淡在信息流里面再也找不见了。所以，常读用户的多少和阅读量有着很大的关系。这也是有一些公众号虽然粉丝很多，但是阅读量并不高的很大原因；同时也是一些公众号虽然粉丝少，但是阅读量一直很高的很大原因。 「在看」功能也是很重要的，当然这取决于文章质量了，如果文章质量高，「在看」多，大家从「朋友在看」入口进入到文章的的人也越多，阅读量自然也会高。 另外还有原创，大家可以看到很多原创率高的号主，阅读量都是很高的，因为原创，内容独特，见解到位，他们的公众号常读用户量和常读用户比率一般都是非常高的，阅读量自然就高了。另外公众平台对于原创也有一定的鼓励和推荐机制，帮助原创号主获得更多的流量。另外现在我观察到技术公众号的一个趋势，就是很多文章都转来转去，大多数公众号的原创率都是很低的，另外广告现在也越来越多（我也参与了），读者又不是傻子，在筛选一篇优质公众号文章越来越难的今天，读者会倾向于阅读原创率高或内容优质的公众号。那些没有原创能力或者内容质量不高的公众号，阅读量增长会非常困难，甚至于淹没于越来越大的公众号海洋之中。 所以，现在运营公众号，最重要的数据是什么？最直接的当然是平均阅读量，这可能直接关乎一个公众号值多少钱或者接到的广告值多少钱。平均阅读量更多取决于什么？「常读用户」和「在看」。所以一方面，我在运营的时候会更注意公众号的发文质量和频率，一定不能长时间不更新，否则万一公众号从「常读的订阅号」列表里面掉出去，就很难找回来了。另外我对于粉丝量其实并没有那么看重了，所以我也很少去参与互推的一些活动。 2019 年我发的文章一部分是原创的技术文，要写的话我会写好，把一些来龙去脉和原理都说清楚，保证文章的质量，但由于时间紧张，原创个人感觉发的并不多。另一部分是转载的一些觉得有价值的技术文，我会和小马一起去挑选一些我们认为还不错的技术文或时效新闻发给大家，希望读者能有所收获。最后就是广告了，2019 年接的广告其实说实话不少，当然接广告也基本上是为了恰口饭，如果大家看到了是广告标题，能帮着戳一戳进去加点阅读量我就非常感激不尽了。其他的互推或者抽奖送书等等活动，我很少很少参与了，一方面觉得意义并没有那么大，对读者也不友好。 我觉得公众号专注于提供优质的内容、见解和想法，这才是好的发展路子，我的涨粉速度肯定没有互推来得快，而且确实也因为我的个人原因对公众号精力投入不够，导致阅读量增长比较慢，但我觉得这是适合我的初衷的发展路子，也是我比较舒服的运营方式。所以，接下来我还是秉承的之前的运营理念，公众号的方向还会专注于技术，致力去提供优质的内容给大家。 另外我也有一些新的运营想法。我一直有关注一个公众号叫做「未闻Code」，公号主是「青南」，他是做网络爬虫方向的，也著有很不错的技术书籍，也在维护一个开源项目 GNE，即新闻网页正文通用抽取器，项目地址：<a href="https://github.com/kingname/GeneralNewsExtractor，可以实现新闻页面的自动化抽取，目前已经有" target="_blank" rel="noopener">https://github.com/kingname/GeneralNewsExtractor，可以实现新闻页面的自动化抽取，目前已经有</a> 1k 多个 Star，推荐大家关注下。他的公众号有一个我觉得很不错的运营模式，那就是「一日一技」，他会把一些总结或新学到的技能整理出来发到公众号上，有的文章内容可能并不长，可能就是记录自己学习或踩坑的过程，甚至可能就是一个个小的零碎的知识点，但我感觉还是很有价值的，读者反响也不错。而我之前在写文章的时候，我会必须要把一个知识点扩得很大，把知识点或项目的来龙去脉或者完整的使用流程写一篇长文再发出来，因此大家可以看到我的技术原创文一般都会显得比较完整甚至叫啰嗦，为写这篇文章，我可能要去搜罗各种资料，可能也去新学一些新的东西，这样也致使我写一篇文章耗费的时间也会比较长。所以，我想寻求一个转变，我想，比如某天我在工作中解决了一个什么问题，或者我学到了一个小的骚操作，或者我就学到了一个小的知识点，我想也把它写下来，把这件事稍微说明白就行，暂时不去把所有的涉及这个知识点的的东西完整总结。比如我今天刚学到了 Kubernetes 在部署时动态替换环境变量的骚操作，我就只把这个记录下来，分享给大家，不再去展开讲。这样可以提高我的产量，同时把我今天学到的或想法写下来与大家分享，可能文章比较短，可能知识点描述得不够全，但我觉得是一个不错的路子。后面我会尝试下这个方案，如果得到的反响不错的话，我会继续坚持。</p>
                  <h3 id="开源"><a href="#开源" class="headerlink" title="开源"></a>开源</h3>
                  <p>作为一名程序猿，比起刷抖音，我更喜欢逛 GitHub，同时自己也会喜欢写一些开源项目并发到 GitHub 上面，如果能收获一些 Star，心里会有很大的成就感。 先说一下目前的数据吧，我的 GitHub 地址是：<a href="https://github.com/Germey，目前粉丝数" target="_blank" rel="noopener">https://github.com/Germey，目前粉丝数</a> 4.9k，收获 Star 数约 4k，2019 年 Commits 数量 1053 次，目前主要维护了 Gerapy 和 ModelZoo 两个项目，还有一些其他的项目如 ProxyPool、CookiesPool 也有一些人在使用。 由于时间问题，2019 年我在开源这一方面的贡献并不好，Gerapy 和 ModelZoo 两个项目也有一段时间的停更，导致现在也一直不瘟不火，Star 数也一直不多。 我觉得能够有自己拿得出手的开源项目确实是一件很有成就感的事情，新的一年，我会投入更多的精力参与到上面来，目前还是会专注于 Gerapy 和 ModelZoo 两个项目上面来。同时随着学习和积累，可能还会酝酿出新的项目。新的一年，继续加油。</p>
                  <h3 id="写作"><a href="#写作" class="headerlink" title="写作"></a>写作</h3>
                  <p>关注我的读者可能知道我在 2018 年 4 月出版了一本《Python3网络爬虫开发实战》，这也是我写的第一本书，其目前销量已经远远超过我的预期，到现在为止不到两年时间，累积印刷 15 次，印刷量 7w 多，豆瓣评分 9.0 分，也已经被很多学校或培训机构当做教材或辅导书，这些都是我之前没有预料到的，同时这本书也为我带来了一笔可观的收入。 但免不了的，讲爬虫，网站不会是一成不变的，网站一改版，整个案例就跑不通了。这本书，现在挺多案例已经过期了，书稿的内容不好直接修改，我只能在 GitHub 上尽量去跟进修改，但对于一些初学者来说，是很不友好的。另外，爬虫技术日新月异，很多技术或框架已经过时了，另外也出现了一些新的技术和知识点，当时在写书的时候并没有提及到。 所以，我去年就跟编辑策划了《Python3网络爬虫开发实战》第二版的撰写。本次第二版相对于第一版来说，修订了过期的案例，补充了新的知识点。第二版为每个知识点的实战项目对接了针对性的练习平台，避免了案例过期的问题。另外主要增加了异步爬虫、JavaScript 逆向、App 逆向、智能网页解析、深度学习识别验证码、Kubernetes 运维及部署等知识点，同时各个爬虫知识点涉及到的请求、存储、解析、测试等工具也进行了丰富和更新。 到现在算是基本完稿了，现在已经在审稿了，但我还想修改和增加一部分内容。比如最近提议出来的修订过期案例的问题，这个问题很重要，不然不知道啥时候我书里的案例就又过期了，为此我自建了爬虫案例平台，项目在这里：<a href="https://github.com/Germey/Scrape，最近忙着迁移和开发，现在正在把一些案例修改到案例平台上面。其他的稿子差不多了，正在审核中。所以基本上我现在是边改边审的状态，也希望能提前出版的时间。我知道有些读者很急，也盼着第二版的出版" target="_blank" rel="noopener">https://github.com/Germey/Scrape，最近忙着迁移和开发，现在正在把一些案例修改到案例平台上面。其他的稿子差不多了，正在审核中。所以基本上我现在是边改边审的状态，也希望能提前出版的时间。我知道有些读者很急，也盼着第二版的出版</a>。 现在没几天就会有读者问我第二版什么时候出版呀？情况，就是上面这个样子，已经开始审稿了，可能还得几个月吧，争取 2020 上半年可以出来，如有消息，我一定第一时间通知大家。 当然写作也不仅仅是写书，也包括日常的积累。 我自己在平时的工作和学习的过程中也会记一些笔记。我现在把我所有的笔记都用 Typora 这个 MarkDown 编辑工具写下来，然后整理和同步到 GitHub 和 GitBook 上面，我分了好几个记事本，有技术类、生活类、书稿类、开源文档类，整理和总结了不少东西，挺多东西并没有公开发出来，原因我也在上文「公众号」一节提及了一下，但我也想寻求一个新的运营方式，所以我准备把一些自己整理的东西，即便是小的知识点，也都发出来，跟大家一起学习和探讨。 不怕被笑话「原来我这个知识点还不会呢」，我新学到的就发出来看。因为只有「改革开放」才能真正地进步，固步自封最终吃亏的还是自己。</p>
                  <h3 id="知识"><a href="#知识" class="headerlink" title="知识"></a>知识</h3>
                  <p>说到知识，今年来我个人觉得学习的还算及格，我学到的知识一方面来自于工作，一方面来自于平时生活。 稍微总结一下今年来都学了些什么吧：</p>
                  <ul>
                    <li>C#、.NET。其实在实习期间我不接触线上代码，C#、.NET 并不常用。正式开始工作了，这个必须学起来了，因为一些 Service 必须要用它来搭。学了之后，确实觉得 C# 设计得真的很棒，很多特性和理念值得好好学习。</li>
                    <li>爬虫逆向。在 2019 年之前，我对爬虫逆向可以说基本不了解，因为在写第一版书那段期间，网站采取混淆或加密的不多，App 抓包基本都能抓得到。后来时代变了，网站现在你没有个混淆，基本就不是个合格的网站，很多 App 接口抓包也搞不到，或者一些接口加了很多加密参数。所以说 JavaScript 逆向和 App 逆向不学基本上就没法玩爬虫了。所以我也在开始学习和了解这一部分的内容，在 2019 下半年加入了夜幕团队，团队有几位搞逆向非常厉害的大佬，同时我们也合作出了一套 JavaScript 逆向课，另一方面也为了写书做准备。总之，收获很大，也非常感谢大家的指导和帮助。但由于这个技术比较敏感，担心发出来被对方寄律师函什么的，所以我也几乎不发文。不过现在我有了新的思路了，自建爬虫案例平台，所以等建好了，我会发一些关于逆向方面的文章的，大家敬请期待。</li>
                    <li>Kubernetes、DevOps。现在 DevOps 和 Kubernetes 基本上可以说是大势了，部署一把梭。在工作中我们也慢慢地把一些服务迁移到 Kubernetes 上面，为此我也自己摸索和搭建过 Kubernetes 集群，搞了一些 Service、数据库的搭建，DevOps 一套主要用 Azure Pipelines、GitHub Actions，是真香！现在我的几乎所有服务都在往 Kubernetes 上面迁，新的爬虫案例平台也用了 GitHub Actions 来实现自动部署。</li>
                    <li>深度学习。在 2019 年之前，我也有一些深度学习的基础，但 2019 年我在实现一些自己的开源项目 ModelZoo 的的时候，又学习了一些新的模型，把 ModelZoo 重新迁移到 TensorFlow 2.0 上面。另外在工作之余也学习了一些新的模型，如序列标注相关、图像识别相关。但最近我又有了新的想法，用了一段时间 TensorFlow 2.0 之后，感觉有些地方实现起来还是别扭，对接了 Keras 之后，调试也还是不太方便。经过与一些大佬的交流，决定准备转 PyTorch 了，这真可能是趋势，不知道我的感觉对不对。但选择比努力更重要吗不是？方向感觉不对，就要及时调整，没毛病。（逃</li>
                    <li>各种开源库。这些也不算系统的知识点了，单纯就是逛 GitHub 看到的，比如一些实用的类库，比如 typing、loguru、retrying、faker、airtest 等等，学了，顺带写一篇总结文，慢慢积累下来。</li>
                  </ul>
                  <p>总的来说，2019 学到的新东西还算不少，慢慢地我也摸清了我的技术路线，现在还会是 Python 主力的全栈方向，将来可能还会变，一些技术栈我会去慢慢补齐，我知道自己哪些不会，为了达成我的一些目标还需要去学什么。 另外在学习过程中，思考和总结是非常重要的。</p>
                  <ul>
                    <li>遇到不会的，多去思考和搜索，实在不行求助别人。</li>
                    <li>解决了问题，记得复盘和梳理下来。</li>
                    <li>学习新知识，顺带把学到的整理和记录下来。</li>
                  </ul>
                  <p>个人觉得这样学习起来，效果还是不错的。新的一年，继续加油。 当然除了技术，我自己也在学一些其他的，比如英语，之前跟小马出去塞尔维亚玩了一趟，英语要么听不懂，要么说不出来，太难受了。现在上班路上，我会抽时间听一点 BBC，用的是网易云。大家都在用啥学英语啊？ 另外理财相关的知识，我也在了解，同时买了点基金试试水，不过我还觉得储备的知识还不够，还需要继续学习。</p>
                  <h3 id="健身"><a href="#健身" class="headerlink" title="健身"></a>健身</h3>
                  <p>这个话题，真的有点让我难以开口。因为这个健身，我真是做的太失败了，我一年几乎没有健身几次，加上吃太好，从 116 斤胖到了 141 斤。141 - 116 = 25，没错，我一年胖了 25 斤！ 一方面，小马带我吃的太好了，哈哈哈哈，我们几乎每周都会出去吃好吃的，另外她还会给我买各种零食和好吃的，家里的零食一箱箱永远吃不完。 有人说了，你胖了这么多，人家小马怎么瘦了呀？别找借口了。偷偷抹眼泪，说好一起变胖呢？ 另一方面，也是不运动，一坐一天，晚上还没啥空去健身房，日积月累，就成了这样子了，照片就不要看了，我不会给你们看的。 说到健身，小马几乎每周都会去跳舞或者去健身房，我偶尔周末会跟着去体验几节课，但是上完之后，在家就没有继续炼了。哎，也是确实没太有时间锻炼，也有一个原因就是太懒了，可能后者才是最主要的原因。 但这样下去我的体重就要收不住了。在年前的时候我立了 Flag，一周运动至少两次，主要是跑步，如果参与了一次健身也算，我成功坚持下来了。年后继续！ 我会变瘦的！ 没想到，短短一年，「减肥」这个词居然会落在了我的身上。</p>
                  <h3 id="感情"><a href="#感情" class="headerlink" title="感情"></a>感情</h3>
                  <p>嘿嘿，我和小马一起从 2018 跨到了 2020，在一起三年了（抖机灵。哈哈，其实我们已经在一起 449 天啦！总之和小马在一起的日子特别开心，以至于幸福肥了 25 斤（逃。 想分享的生活有太多，但是又觉得在这里公开秀恩爱有点不好意思，反正就是很幸福哈哈哈。比如一起出去吃好吃的、一起旅游、一起健身、一起拍照、一起画画、一起插玩具、一起玩游戏、一起去看展、一起穿情侣装。有时候我们一起吃到了好吃的就会一起高呼「卧槽这个好吃，卧槽这个也好吃，卧槽这个怎么这么好吃！」，有时候走在路上两人就像小孩子一样牵着手或者追来追去，有时候好长时间（一天）不见面，我们再见到对方就互相扑过去，我们很多时候会在对方面前表现得像个孩子或者像一只动物（猫？），我们每人的表情包已经全是「猫」和「狗」了，不多，也就一百多套吧。不好意思，不小心又秀了一下。 怎么说呢？我经常会在微博分享我们两人的生活。有人说我的微博已经从「互联网资讯博主」变成了「恋爱博主」，就是这么个感觉。本来我的书里面不是写了一节「Ajax 爬取微博」来教大家怎么学 Ajax 分析和爬取吗，结果大家爬下来了一堆狗粮，之后跟我说再也不学爬虫了。 我：？？？ 嗯，我也不知道他后来有没有再学，可能真的没有再学了吗？ 不过有时候我也会犯蠢，惹小马不开心或生气（但我们不会分手的，我也慢慢地从中学到了很多，我们的感情也变得越来越好了。昨天我看到知乎一个推荐「女生最想收到男生送的什么礼物？」，我看了看这都什么玩意，然后发给了小马，说我要是送你这些肯定会被分手了，小马看完，说我长大了，开心！看我是不是变得越来越懂你了呢。 新的一年，希望我们还会继续好好在一起呀！ 对了，朋友圈和微博没得刷了或者饿了的话，来刷刷我的微博「崔庆才丨静觅」吧（逃。</p>
                  <h3 id="思考"><a href="#思考" class="headerlink" title="思考"></a>思考</h3>
                  <p>关于思考，这是一个非常抽象的东西，看不见摸不着，但确实很多时候，某些事情思考的深度、广度决定了我们的高度。 在这一年来，我接触了很多人，了解了一些事。比如一个技术项目吧，不同的人对于一个项目思考的深度就不同。很多人可能就是，接到了一个任务，别人告诉他要做什么，怎么做，那他就做完就行了，然后就完事了。但有的人，接到这个任务，会首先想，我这是在做什么？为什么要这么做而不是那么做？做的时候怎样实现才是最佳的？做完了还能做点什么才能变得更好？他会对事情的来龙去脉了解的非常清楚，结束了再去复盘和思考。这也是我觉得很多人所欠缺的一些地方。我觉得人和人之间段位的差距就是这么拉开的。 有的人可能会觉得，考虑这么多，找这个麻烦干什么呢？这其实真不是自己找麻烦，思考的成本其实很低，我们用来思考的时间其实很多，比如路上、休息时、吃饭时、睡前等等。不怕思考浪费时间，怕的是不去思考。 我前几天跟一位非常好的朋友吃饭聊天。这位朋友，我真的很佩服他，我觉得他有很多程序员少有的一些思维，他在思考一些事情时会考虑非常全面，就像上文所提到的一样。比如我之前曾经跟他商量一个实现方案，他第一个问我的问题是，为什么你会这么想这么做？你的理由是什么？你打算怎么来实现？只要你能把我说得通，我就支持你这么做。比如一些方法论上的东西，他一直在思考和探索一些适合自己的生活、工作方式，会思考自己想要做什么、为了达成某个目标怎样做最好、怎样才是最佳实践，同时他也在不断的探索和试错中完善自己的方法论。我从他身上学到了很多，同时我觉得我自己跟他还有很大的差距，还是要多多加油啊。 所以，有时候，我们也需要停下来去思考，自己在做什么、为什么要这么做、怎样做最好，甚至需要去思考一下，自己是谁、从哪里来、要到哪里去。</p>
                  <h3 id="合作"><a href="#合作" class="headerlink" title="合作"></a>合作</h3>
                  <p>关于合作，今年我也体会到了很多，我的想法也变了许多。 怎么说呢，我自己原本是一个倾向于单枪匹马挑战一切的人，同时也不想去麻烦别人，比如一件事情，我习惯于包揽下来，自己去完成，有时候不想给别人添麻烦，有时候觉得交给别人不太放心或者担心做出来不符合我的心意。 慢慢地，在团队合作过程中，我意识到了，自己不是全能的，术业总会有专攻，总会有在某一方面比自己强的人。而且随着工作强度的变大，有些事情自己大包大揽真的有点力不从心。 两点体会吧。</p>
                  <ul>
                    <li>第一，一个人并不是万能的。每个人不可能在所有的事情上做到极致，同时一个人的精力也是有限的。几乎没有人能像 Linus 一样几乎单枪匹马做出一个 Linux 内核，但你说 Linux 和杜兰特比打篮球，谁更厉害？</li>
                    <li>第二，总会有人在某个方面强过自己。随着接触的人和事越来越多，我发现就是有人在某个方面比自己强，或者思考的问题深入，或者完成的效果好。所以，首先不要因为某个方面不如别人而感到难过，有些事，可以放心交给别人去做，有时候结果可能甚至远超过自己的预期。当然这个有个前提，确实也得看人，去学会分辨一些靠谱和不靠谱的人。</li>
                  </ul>
                  <p>所以，现在我在工作中，一些功能和需求，我不会再像之前一样倾向于大包大揽，相信自己的一些靠谱的合作伙伴，每个人直接好好分配，合作的时候一起交流、探讨。同时我自己也指导着两位同事，有些任务我会交给他们去做，不会再因为不放心和给别人添麻烦而全把任务归到自己。有时候真的，最终可能还会有意想不到的效果，或者在跟别人交流的过程中学到一些新的东西。 所以，Be Open，这是我的一点体会。</p>
                  <h3 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h3>
                  <p>关于这个，我体会也很深。 我回顾了自己一年以来没有做和已经做的事情，发现了这么一个现象：有件事我确实给自己定目标了，比如我要学习 Go 语言。然后我就把这个加到了我的待做清单里面，没有给他设置时间限度，也没有具体规划我怎样去做，哪个时间去学什么，反而自己的时间被一些零碎的或更紧急的事情占据了，最后我一整年都没有学 Go。我仔细想想，其实也并不是没有时间，有时候，我在某个时间段，确实是完全闲着的，比如我周六的时候，可能会躺在床上玩手机，一玩一上午，但那会啥也不想做，也没想好要那会要做什么。 我反思了一下自己，还是因为自己给自己的规划不明确。 主要有这么两点：</p>
                  <ul>
                    <li>第一，某些目标我设置的太大，没有详细去规划什么时间做什么。比如学 Go 语言，我应该去好好思考一下，我要在多久时间内达成这个目标，我应该什么时间去做什么，我应该去细分到每一章节，在最开始的时候可能没必要所有的都分的那么细，但真正下一步要做的，一定要列得详细再详细。 比如说，我要三个月内学好 Go 语言，我可以先思考，三个月，我要学多少知识模块，比如有十个知识模块，那么我就规划每一个模块大体什么时候完成，每个模块列到自己的 Todo List 里面，设定好期限，注意，一定要设置好期限，不然真的会一拖再拖！然后，最开始我可能没必要把大把的时间把每个模块里面的每个小知识点都拆分好，但前面的一定要列好，比如我十个模块，我最开始的一两个模块一定要再拆分规划好，同时再设定好每个小知识点的时间。要是前面的模块学完了，再去抽时间规划下一个模块就好了。</li>
                    <li>第二，没有提前设定好每个小目标。我反思了，为什么有的时候我不知道要干些啥呢？原因就是我没有提前规划好我第二天或者接下来的时间做什么。所以我后面决定改变一下，我会为自己提前做好规划，比如我第二天做什么，以及另外一个很重要的，规划一些零碎的时间做什么，比如学习慕课网的一个视频，或者去学习某一节英语课，或者去完成某项健身活动，把时间都利用起来。</li>
                  </ul>
                  <h3 id="感悟"><a href="#感悟" class="headerlink" title="感悟"></a>感悟</h3>
                  <p>另外，这一年中，我也悟出了一些做事的原则或者生活上的感悟，我把一些体会比较深的写下来。</p>
                  <ul>
                    <li>别自嗨，多往外看看。有时候我们可能新学到了一个知识点，或者新做成了一个功能，就觉得自己很了不起了，但殊不知，可能别人已经把这个知识点当做必备知识，或者我们做出来的这个功能拿到外面去，其实是完全被爆的。所以，不要闭门造车，多出去看看，多了解下别人是怎么做的，多了解下这个的前沿和天花板已经到了什么地步，站在巨人的肩膀上，往往会走得更远。</li>
                    <li>别沉浸于过去。有时候我就会沉浸或满足于自己已经取得的一些成就，去“啃老”，但这样是不对的。不论我现在已经取得了什么成就，我都不应该沉浸进去。要把每一天当成 0 起点去对待，和自己的前一天去比较，每天进步一点，这样日积月累，进步就显现出来了。</li>
                    <li>多反思和复盘。就像刚才说的，我们每个人可能一天上班完了，就回家睡大觉了，然后第二天接着去上班。但有多少人每天晚上回问，自己今天到底进步了什么，即使没有进步，也反思一下自己今天为什么没有进步，怎样来解决这个现状。所以，我觉得每天去反思和复盘是很重要的。睡前的十几二十分钟，去思考一下，今天做成了什么、没有做成什么、下一步该怎么做，我觉得还是非常有价值的。</li>
                    <li>不能打包票的不要许诺。很多事情，我们要量力而行。有时候我答应过别人一件事情，可是到了时候，我发现自己没有时间完成或者没有能力去完成，最后去跟别人说要拖延时间或者干脆不做了。这其实对别人来说也很不好，而且本来可能是帮别人一个忙，反而可能会成为倒忙。所以，一些事情，在答应之前，好好想想到底有没有问题，如果不能打包票，不要轻易许诺。</li>
                    <li>一些小事也别以为很简单，重要的是细节。一件事，做的时候不要眼高手低，本来以为很简单的一件事，觉得分分钟就能做完，结果做完了发现很多细节没有把握好，出了很多错误。比如我记得之前我答应小马改个文章，本来以为很简单的东西，心想不就是改个这个吗，当时改的时候还在想着别的东西，改的时候并没有那么认真，结果导致有些错别字，最后被打回来返工，使得事情变得更糟。我不止一次犯过这种毛病了，犯错之后我也深深自责，为什么这么简单的事也能错。所以，一些小事也不要以为很简单，要认认真真去做，一些细节要把握好。</li>
                  </ul>
                  <p>还有一些别的感悟，有的感悟更深刻了，不过之前都写过了，我就不再写了，大家如果感兴趣可以看看我去年的年度总结或者之前我的一些分享。 好了，一些变化和反思我就暂时总结这么多了，不足的地方还有很多，也希望来年我能变得更好，无愧于自己的努力。</p>
                  <h2 id="目标-1"><a href="#目标-1" class="headerlink" title="目标"></a>目标</h2>
                  <p>照例，新的 2020 年，我给自己立一点 Flag 吧！明年再来继续总结和验收。</p>
                  <h3 id="工作"><a href="#工作" class="headerlink" title="工作"></a>工作</h3>
                  <p>工作继续好好努力，这是最重要的，现在我规划了一些新的尝试的方向，愿明年能够顺利实现和上线。</p>
                  <h3 id="健身减肥"><a href="#健身减肥" class="headerlink" title="健身减肥"></a>健身减肥</h3>
                  <p>新的一年，每个工作周，健身至少 2 次。体重减重到 130 斤并一直维持，减掉赘肉和小肚子。</p>
                  <h3 id="读书"><a href="#读书" class="headerlink" title="读书"></a>读书</h3>
                  <p>给自己定一个读书计划，读完自己规划的一些书，每一本写出自己的感悟。</p>
                  <h3 id="日-周总结"><a href="#日-周总结" class="headerlink" title="日/周总结"></a>日/周总结</h3>
                  <p>每日复盘和总结，写到日记本中，每周末对该周的内容进行复盘和总结。</p>
                  <h3 id="爬虫书"><a href="#爬虫书" class="headerlink" title="爬虫书"></a>爬虫书</h3>
                  <p>《Python3网络爬虫开发实战（第二版）》书籍完成，书籍争取在上半年发售。另外还规划了配套视频，按照计划顺利出来。</p>
                  <h3 id="公众号-1"><a href="#公众号-1" class="headerlink" title="公众号"></a>公众号</h3>
                  <p>公众号我就不按照粉丝量来设定目标了，如果「常读用户」机制一直存在的话，新的一年，我希望公众号常读用户数目可以达到 2w，平均阅读量达到 6000，即翻倍。 另外公众号会尝试新的运营思路，我会写一些小的知识点发到公众号上，如果反响不错，新的一年会一直保持。</p>
                  <h3 id="开源-1"><a href="#开源-1" class="headerlink" title="开源"></a>开源</h3>
                  <p>继续维护自己的项目 Gerapy 和 ModelZoo。 Gerapy 把已经规划好的「可视化爬虫」、「智能解析」、「监控分析」等功能完善，Star 数破 3k。 ModelZoo 将其迁移到 PyTorch，并对接好当前规划的前沿主流模型，总 Star 数破 1k。</p>
                  <h3 id="理财"><a href="#理财" class="headerlink" title="理财"></a>理财</h3>
                  <p>学习一些理财知识，记录成自己的一套方法论。</p>
                  <h3 id="感情-1"><a href="#感情-1" class="headerlink" title="感情"></a>感情</h3>
                  <p>当然是和小马好好在一起！让我们的感情变得更好！</p>
                  <h3 id="收入"><a href="#收入" class="headerlink" title="收入"></a>收入</h3>
                  <p>这个自己给自己定了一个目标，这个具体数字我不说啦，朝着我的小米之家梦进发！</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-01-29 10:35:13" itemprop="dateCreated datePublished" datetime="2020-01-29T10:35:13+08:00">2020-01-29</time>
                </span>
                <span id="/8808.html" class="post-meta-item leancloud_visitors" data-flag-title="2019 年终总结：新生活、新探索" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>12k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>11 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8703.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8703.html" class="post-title-link" itemprop="url">新书发售 限时折扣｜《Python3 反爬虫原理与绕过实战》</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>无论是在学习还是工作中，反爬虫技术是所有爬虫工程师都要面对的问题。 常见的反爬虫原理和绕过技巧也是中高级爬虫工程师<strong>面试中关注的焦点</strong>， 尤其是那些竞争激烈的大型互联网企业。作为一名<strong>开发者</strong>，了解<strong>反爬虫原理</strong>和<strong>绕过技巧</strong>有助于<strong>设计</strong>出更合理的<strong>反爬虫策略</strong>，这会使你在同行中<strong>脱颖而出</strong>，<strong>大放异彩</strong>。</p>
                  <h1 id="那么问题来了"><a href="#那么问题来了" class="headerlink" title="那么问题来了"></a>那么问题来了</h1>
                  <p>如何<strong>深入</strong>学习<strong>反爬虫原理</strong>并<strong>掌握绕过技巧</strong>呢？ 今天给大家推荐业内深受欢迎的反爬虫专题书籍《Python3 反爬虫原理与绕过实战》</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191214150839.jpeg" alt="" title="null"></p>
                  <p>这本书于 2020 年 1 月出版，目前在各大电商平台和书城均有售。本书定价 89，现在各大平台均有不同的<strong>限时折扣</strong>，喜欢的朋友赶紧下手哦！ 【京东自营】 <a href="https://item.jd.com/12794078.html" target="_blank" rel="noopener">https://item.jd.com/12794078.html</a> 【天猫】<a href="https://detail.tmall.com/item.htm?spm=a230r.1.14.201.15272c73Ta0USk&amp;id=611222843708&amp;ns=1&amp;abbucket=7" target="_blank" rel="noopener">https://detail.tmall.com/item.htm?spm=a230r.1.14.201.15272c73Ta0USk&amp;id=611222843708&amp;ns=1&amp;abbucket=7</a> 【当当】<a href="http://product.dangdang.com/28508464.html" target="_blank" rel="noopener">http://product.dangdang.com/28508464.html</a> 书中描述了爬虫技术与反爬虫技术的<strong>对抗过程</strong>，并详细介绍了这其中的<strong>原理</strong>和具体的<strong>实现方法</strong>。本书从开发环境的配置到 Web 网站的构成和页面渲染，再到动态网页和静态网页对爬虫造成的影响。然后介绍了不同类型的<strong>反爬虫原理</strong>、<strong>具体实现</strong>和<strong>绕过方法</strong>。书中还讲解了<strong>常见验证码的实现过程</strong>，并使用<strong>深度学习技术完成了验证</strong>。最后介绍了常见的<strong>编码和加密原理</strong>、<strong>JavaScript 代码混淆</strong>知识、<strong>前端禁止事件</strong>以及<strong>与爬虫相关的法律知识和风险点</strong>。</p>
                  <h1 id="精彩抢先看"><a href="#精彩抢先看" class="headerlink" title="精彩抢先看"></a>精彩抢先看</h1>
                  <p>在原理探究和分析方面，你会经历细致的分析过程，并通过示意图加深对知识的理解。例如第 6 章第 2 节 CSS 偏移反爬虫中描述元素位置和样式值关系的示意图：</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191231134122.png" alt="" title="null"></p>
                  <p>例如第 6 章第 3 节 SVG 反爬虫中描述 SVG text 定位的示意图：</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191231134610.jpg" alt="" title="null"></p>
                  <p>例如第 10 章第 1 节编码与加密中描述加密过程的示意图：</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191231134642.png" alt="" title="null"></p>
                  <p>例如第 9 章第 3 节滑动验证码中描述移动距离的示意图：</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191231134827.png" alt="" title="null"></p>
                  <p>网站的反爬虫措施是会更新的，为了保证读者的学习质量，本书在编写过程中开发了一套拥有 21 个示例的练习平台 Steamboat。</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191231135742.jpg" alt="" title="null"></p>
                  <p>练习平台与书本紧密结合，不会出现学习过程中找不到与书本相同环境的情况，同时也能避免因练习而导致的侵权问题。除了配套的示例之外，书中还分析了众多互联网产品中使用到的反爬虫手段，这些产品包括大众点评、淘宝滑动验证码、猫眼电影、京东商城、去哪儿网、掘金社区和掌上英雄联盟等。 你有想过将深度学习应用到爬虫中吗？</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191231140847.jpg" alt="" title="null"></p>
                  <p>书中介绍了如何通过卷积神经网络来应对字符验证码，并给出了训练用的图片和识别率高达 99% 的训练代码。其中部分代码如下：</p>
                  <figure class="highlight arcade">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="string">`folders \= PATH_TEST # 指定预测集路径`</span><span class="string">`trains \= get_image_name(PATH_TRAIN)  # 获取训练样本所有图片的名称`</span><span class="string">`pres \= get_image_name(folders)  # 获取预测集所有图片的名称`</span><span class="string">`repeat \= len([p for p in pres if p in trains])  # 获取重复数量`</span><span class="string">`start_verifies(folders)  # 开启预测`</span><span class="string">`logging.info('预测前确认待预测图片与训练样本的重复情况，'`</span><span class="string">`'待预测图片%s张，训练样本%s张，重复数量为%s张' % (len(pres), len(trains), repeat))`</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>当然，还有通过目标检测算法来应对点选验证码的精彩章节。</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191231141853.png" alt="" title="null"></p>
                  <h1 id="这本书是谁写的？"><a href="#这本书是谁写的？" class="headerlink" title="这本书是谁写的？"></a>这本书是谁写的？</h1>
                  <p>作者韦世东是一名资深爬虫工程师，2019年华为云认证云享专家、掘金社区优秀作者、GitChat认证作者、夜幕团队 NightTeam 的成员。</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102171141.png" alt="" title="null"></p>
                  <p>他曾在掘金社区发布过电子小册《Python 实战：用 Scrapyd 打造个人化的爬虫部署管理控制台[1]》 。也在 GitChat 上发布过 MongoDB 的 10 万字教程《超高性价比的 MongoDB 零基础快速入门实战教程[2]》。还在华为总部进行过时长 2 小时的技术直播，直播主题为《Python 项目部署与调度核心逻辑[3]》。</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102171245.jpg" alt="" title="null"></p>
                  <h1 id="这本书适合哪些朋友？"><a href="#这本书适合哪些朋友？" class="headerlink" title="这本书适合哪些朋友？"></a>这本书适合哪些朋友？</h1>
                  <p>这本书的目标读者分为两个阵营：<strong>爬虫</strong>和<strong>反爬虫</strong>。 爬虫工程师自然不用多说，大家最期待的正是对反爬虫技术的剖析和绕过实战。 反爬虫的设计者和实施者遍布于各个岗位，它可以是<strong>前端工程师</strong>、<strong>后端工程师</strong>、<strong>移动端研发</strong>甚至是<strong>产品经理</strong>。他们能够<strong>从书中了解到爬虫工程师常用的技术手段和思路</strong>，知道<strong>哪些防护措施容易被突破</strong>、<strong>哪些措施的绕过难度会更高</strong>以及<strong>如何限制爬虫</strong>，从而<strong>设计出适合的反爬虫策略</strong>。</p>
                  <h1 id="大厂高级研发怎么看？"><a href="#大厂高级研发怎么看？" class="headerlink" title="大厂高级研发怎么看？"></a>大厂高级研发怎么看？</h1>
                  <p>以下是几位大厂工程师为本书编写的推荐语。</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102175332.jpg" alt="" title="null"></p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102175558.jpg" alt="" title="null"></p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102175614.jpg" alt="" title="null"></p>
                  <h1 id="详细的章节目录"><a href="#详细的章节目录" class="headerlink" title="详细的章节目录"></a>详细的章节目录</h1>
                  <p>详细目录如下：</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102165750.jpg" alt="" title="null"></p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102165805.jpg" alt="" title="null"></p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102165825.jpg" alt="" title="null"></p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20200102165838.jpg" alt="" title="null"></p>
                  <p>这简直就是手把手带你探寻反爬虫的世界！ <img src="https://qiniu.cuiqingcai.com/wp-content/uploads/2020/01/0f042d65-8e75-484b-89c0-b2b944a66661_0.png" alt=""></p>
                  <h3 id="References"><a href="#References" class="headerlink" title="References"></a>References</h3>
                  <p><code>[1]</code> Python 实战：用 Scrapyd 打造个人化的爬虫部署管理控制台: <em><a href="https://juejin.im/book/5bb5d3fa6fb9a05d2a1d819a/section" target="_blank" rel="noopener">https://juejin.im/book/5bb5d3fa6fb9a05d2a1d819a/section</a></em> <code>[2]</code> 超高性价比的 MongoDB 零基础快速入门实战教程: <em><a href="https://gitbook.cn/gitchat/activity/5d52baeaac15fd68e9f78297" target="_blank" rel="noopener">https://gitbook.cn/gitchat/activity/5d52baeaac15fd68e9f78297</a></em> <code>[3]</code> Python 项目部署与调度核心逻辑: <em><a href="http://huaweicloud.bugu.mudu.tv/watch/vondje76" target="_blank" rel="noopener">http://huaweicloud.bugu.mudu.tv/watch/vondje76</a></em></p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/韦世东学算法和反爬虫" class="author" itemprop="url" rel="index">韦世东学算法和反爬虫</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2020-01-03 19:55:51" itemprop="dateCreated datePublished" datetime="2020-01-03T19:55:51+08:00">2020-01-03</time>
                </span>
                <span id="/8703.html" class="post-meta-item leancloud_visitors" data-flag-title="新书发售 限时折扣｜《Python3 反爬虫原理与绕过实战》" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>2.2k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8678.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8678.html" class="post-title-link" itemprop="url">揭秘去哪儿网在用的 CSS 偏移反爬虫手段！</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>内容选自<strong>即将出版</strong>的《Python3 反爬虫原理与绕过实战》，本次公开书稿范围为第 6 章——文本混淆反爬虫。本篇为第 6 章中的第 2 小节，第 3、4 小节已发，直达链接：</p>
                  <ul>
                    <li>《<a href="https://juejin.im/post/5e05a58b6fb9a0164f2955b2" target="_blank" rel="noopener">一线大厂在用的反爬虫手段，看我破！</a>》</li>
                    <li>《<a href="https://juejin.im/post/5e03ef93518825125c4316c3" target="_blank" rel="noopener">用前考虑清楚，伤敌一千自损八百的字体反爬虫</a>》</li>
                  </ul>
                  <p>其余小节将<strong>逐步放送</strong>。</p>
                  <h2 id="CSS-偏移反爬虫"><a href="#CSS-偏移反爬虫" class="headerlink" title="CSS 偏移反爬虫"></a>CSS 偏移反爬虫</h2>
                  <p>CSS 偏移反爬虫指的是利用 CSS 样式将乱序的文字排版为人类正常阅读顺序的行为。这个概念不是很好理解，我们可以通过对比两段文字来加深对这个概念的理解。</p>
                  <ul>
                    <li>HTML 文本中的文字：我的学号是 1308205，我在北京大学读书。</li>
                    <li>浏览器显示的文字：我的学号是 1380205，我在北京大学读书。</li>
                  </ul>
                  <p>爬虫提取到的学号是 1308205，但用户在浏览器中看到的却是 1380205。如果不细心观察，爬虫工程师很容易被爬取结果糊弄。这种混淆方法和图片伪装一样，是不会影响用户阅读的。让人好奇的是，浏览器如何将 HTML 文本中的数字按照开发者的意愿排序或放置呢？这种放置规则是如何运作的呢？我们可以通过一个具体的例子来了解 CSS 偏移反爬虫的应用和绕过方法。</p>
                  <h3 id="6-2-1-CSS-偏移反爬虫绕过实战"><a href="#6-2-1-CSS-偏移反爬虫绕过实战" class="headerlink" title="6.2.1 CSS 偏移反爬虫绕过实战"></a>6.2.1 CSS 偏移反爬虫绕过实战</h3>
                  <p>示例 5：CSS 偏移反爬虫示例。 网址：<a href="http://www.porters.vip/confusion/flight.html" target="_blank" rel="noopener">http://www.porters.vip/confusion/flight.html</a>。 任务：爬取航班查询和机票销售网站页面中的航站名称、所属航空公司和票价，页面内容如图 6-4 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225223139.jpg" alt=""> 图 6-4 示例 5 页面 在编写 Python 代码之前，我们需要确定目标数据的元素定位。航空公司名称元素定位如图 6-5 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225223224.jpg" alt=""> 图 6-5 航空公司名称元素定位结果 航空公司名称包裹在没有属性的 span 标签中，但该 span 标签包裹在 class 属性为 air g-tips 的 div 标签中。接下来我们看一下航站名称的元素定位，定位结果如图 6-6 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225223314.jpg" alt=""> 图 6-6 航站名称元素定位结果 航站名称包裹在没有属性的 h2 标签中，h2 标签包裹在 class 为 sep-lf 的 div 标签中。 我们再看一下票价的元素定位，定位结果如图 6-7 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225223407.jpg" alt=""> 图 6-7 票价的元素定位结果 页面中显示的票价为 467，但是在网页中却有两组不同的数字，其中一组是[7, 7, 7]，而另一组是 [6, 4]，这看起来就有点奇怪了。 难道是网页显示有问题？ 按照正常排序来说，这架航班的票价应该是 77 764 才对。我们可以查看第二架航班信息的价格，思考是网页显示问题还是做了什么反爬虫措施。第二架航班的票价元素定位结果如图 6-8 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225223516.jpg" alt=""> 图 6-8 第二架航班的票价元素定位结果 结果与第一架航班的票价显示有同样的问题：网页显示内容和 HTML 代码中的内容不一致。我们分析一下 HTML 代码，看一看是否能找到什么线索。第一架航班票价的 HTML 代码为：</p>
                  <figure class="highlight xml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">"prc_wp"</span> <span class="attr">style</span>=<span class="string">"width:48px"</span>&gt;</span> </span><br><span class="line">    <span class="tag">&lt;<span class="name">em</span> <span class="attr">class</span>=<span class="string">"rel"</span>&gt;</span> </span><br><span class="line">        <span class="tag">&lt;<span class="name">b</span> <span class="attr">style</span>=<span class="string">"width:48px;left:-48px"</span>&gt;</span> </span><br><span class="line">            <span class="tag">&lt;<span class="name">i</span> <span class="attr">style</span>=<span class="string">"width: 16px;"</span>&gt;</span>7<span class="tag">&lt;/<span class="name">i</span>&gt;</span> </span><br><span class="line">            <span class="tag">&lt;<span class="name">i</span> <span class="attr">style</span>=<span class="string">"width: 16px;"</span>&gt;</span>7<span class="tag">&lt;/<span class="name">i</span>&gt;</span> </span><br><span class="line">            <span class="tag">&lt;<span class="name">i</span> <span class="attr">style</span>=<span class="string">"width: 16px;"</span>&gt;</span>7<span class="tag">&lt;/<span class="name">i</span>&gt;</span> </span><br><span class="line">        <span class="tag">&lt;/<span class="name">b</span>&gt;</span> </span><br><span class="line">        <span class="tag">&lt;<span class="name">b</span> <span class="attr">style</span>=<span class="string">"width: 16px;left:-32px"</span>&gt;</span>6<span class="tag">&lt;/<span class="name">b</span>&gt;</span> </span><br><span class="line">        <span class="tag">&lt;<span class="name">b</span> <span class="attr">style</span>=<span class="string">"width: 16px;left:-48px"</span>&gt;</span>4<span class="tag">&lt;/<span class="name">b</span>&gt;</span> </span><br><span class="line">    <span class="tag">&lt;/<span class="name">em</span>&gt;</span> </span><br><span class="line"><span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>代码中有 3 对 b 标签，第 1 对 b 标签中包含 3 对 i 标签，i 标签中的数字都是 7，也就是说第 1 对 b 标签的显示结果应该是 777。而第 2 对 b 标签中的数字是 6，第 3 对 b 标签中的数字是 4。 这些数字与页面所显示票价 467 的关系是什么呢？ 这一步找到的标签和数字有可能是数据源，但是数字的组合有很多种可能，如图 6-9 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225223638.jpg" alt=""> 图 6-9 数字组合推测 5 个数字的组合结果太多了，我们必须找出其中的规律，这样就能知道网页为什么显示 467 而不是 764 或者 776 。在仔细查看过后，发现每个带有数字的标签都设定了样式。第 1 对 b 标签的样式为：</p>
                  <figure class="highlight scss">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">width</span>:<span class="number">48px</span>;<span class="attribute">left</span>:-<span class="number">48px</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>第 2 对 b 标签的样式为：</p>
                  <figure class="highlight scss">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">width</span>: <span class="number">16px</span>;<span class="attribute">left</span>:-<span class="number">32px</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>第 3 对 b 标签的样式为：</p>
                  <figure class="highlight scss">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">width</span>: <span class="number">16px</span>;<span class="attribute">left</span>:-<span class="number">48px</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>i 标签对的样式是相同的，都是：</p>
                  <figure class="highlight scss">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">width</span>: <span class="number">16px</span>;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>另外，还注意到最外层的 span 标签对的样式为：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">width:<span class="number">48</span>px</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>如果按照 CSS 样式这条线索来分析的话，第 1 对 b 标签中的 3 对 i 标签刚好占满 span 标签对的位置，其位置如图 6-10 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225223828.jpg" alt=""> 图 6-10 span 标签对和 i 标签对位置图 此时网页中显示的价格应该是 777，但是由于第 2 和第 3 对 b 标签中有值，所以我们还需要计算它们的位置。此时标签位置的变化如图 6-11 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225223941.jpg" alt=""> 图 6-11 标签位置变化 右侧是标签位置变化后的结果，由于第 2 对 b 标签的位置样式是 left:-32px，所以第 2 对 b 标签中的值 6 就会覆盖原来第 1 对 b 标签中的中的第 2 个数字 7，此时页面应该显示的数字是 767。 按此规律推算，第 3 对 b 标签的位置样式是 left:-48px，这个标签的值会覆盖第 1 对 b 标签中的第 1 个数字 7，覆盖结果如图 6-12 所示，最后显示的票价是 467。 <img src="http://can.sfhfpc.com/sfhfpc/20191225224032.jpg" alt=""> 图 6-12 覆盖结果 根据结果来看这种算法是合理的，不过我们还需要对其进行验证，现在将第二架航班的 HTML 值 和 CSS 样式按照这个规律进行推算。最后推算得到的结果与页面显示结果相同，说明这个位置偏移的计算方法是正确的，这样我们就可以编写 Python 代码获取网页中的票价信息了。因为 b 标签包裹在 class 属性为 rel 的 em 标签下，所以我们要定位所有的 em 标签。对应的 Python 代码如下：</p>
                  <figure class="highlight xl">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> requests </span><br><span class="line"><span class="keyword">import</span> re </span><br><span class="line">from parsel <span class="keyword">import</span> Selector </span><br><span class="line">url = <span class="string">'http://www.porters.vip/confusion/flight.html'</span> </span><br><span class="line">resp = requests.get(url) </span><br><span class="line">sel = Selector(resp.<span class="keyword">text</span>) </span><br><span class="line">em = sel.css(<span class="string">'em.rel'</span>).extract()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>接着定位所有的 b 标签。由于 b 标签中还有 i 标签，而且 i 标签的值是基准数据，所以可以直接提取。对应的 Python 代码如下：</p>
                  <figure class="highlight livecodeserver">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">for</span> <span class="keyword">element</span> <span class="keyword">in</span> em: </span><br><span class="line">    <span class="keyword">element</span> = Selector(<span class="keyword">element</span>) </span><br><span class="line">    <span class="comment"># 定位所有的&lt;b&gt;标签</span></span><br><span class="line">    element_b = <span class="keyword">element</span>.css(<span class="string">'b'</span>).extract() </span><br><span class="line">    b1 = Selector(element_b.pop(<span class="number">0</span>)) </span><br><span class="line">    <span class="comment"># 获取第 1 对&lt;b&gt;标签中的值(列表) </span></span><br><span class="line">    base_price = b1.css(<span class="string">'i::text'</span>).extract()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>接下来要提取其他 b 标签的偏移量和数字。对应的 Python 代码如下：</p>
                  <figure class="highlight cs">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">alternate_price = [] </span><br><span class="line"><span class="keyword">for</span> eb <span class="keyword">in</span> element_b: </span><br><span class="line">   eb = Selector(eb) </span><br><span class="line">   <span class="meta"># 提取&lt;b&gt;标签的 style 属性值</span></span><br><span class="line">   style = eb.css(<span class="string">'b::attr("style")'</span>).<span class="keyword">get</span>() </span><br><span class="line">   <span class="meta"># 获得具体的位置</span></span><br><span class="line">   position = <span class="string">''</span>.<span class="keyword">join</span>(re.findall(<span class="string">'left:(.*)px'</span>, style)) </span><br><span class="line">   <span class="meta"># 获得该标签下的数字</span></span><br><span class="line">   <span class="keyword">value</span> = eb.css(<span class="string">'b::text'</span>).<span class="keyword">get</span>() </span><br><span class="line">   <span class="meta"># 将&lt;b&gt;标签的位置信息和数字以字典的格式添加到替补票价列表中</span></span><br><span class="line">   alternate_price.append(&#123;<span class="string">'position'</span>: position, <span class="string">'value'</span>: <span class="keyword">value</span>&#125;)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>然后根据偏移量决定基准数据列表的覆盖元素，实际上是完成图 6-11 中的操作。</p>
                  <figure class="highlight vim">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">for</span> <span class="keyword">al</span> in alternate_price: </span><br><span class="line">   position = <span class="keyword">int</span>(<span class="keyword">al</span>.<span class="built_in">get</span>(<span class="string">'position'</span>)) </span><br><span class="line">   value = <span class="keyword">al</span>.<span class="built_in">get</span>(<span class="string">'value'</span>) </span><br><span class="line">   # 判断位置的数值是否正整数</span><br><span class="line">   plus = True <span class="keyword">if</span> position &gt;= <span class="number">0</span> <span class="keyword">else</span> False </span><br><span class="line">   # 计算下标，以 <span class="number">16</span>px 为基准</span><br><span class="line">   <span class="built_in">index</span> = <span class="keyword">int</span>(position / <span class="number">16</span>) </span><br><span class="line">   # 替换第一对<span class="symbol">&lt;b&gt;</span>标签值列表中的元素，也就是完成值覆盖操作</span><br><span class="line">   base_price[<span class="built_in">index</span>] = value </span><br><span class="line"><span class="keyword">print</span>(base_price)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>最后将数据列表打印出来，得到的输出结果为：</p>
                  <figure class="highlight scheme">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[<span class="symbol">'4</span>', <span class="symbol">'6</span>', <span class="symbol">'7</span>'] </span><br><span class="line">[<span class="symbol">'8</span>', <span class="symbol">'7</span>', <span class="symbol">'0</span>', <span class="symbol">'5</span>']</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>令人感到奇怪的是，输出结果中第一组票价数字与页面中显示的相同，但第二组却不同。这是因为第二架航班的票价基准数据有 4 个值。航班票价对应的 HTML 代码如下：</p>
                  <figure class="highlight xml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="tag">&lt;<span class="name">em</span> <span class="attr">class</span>=<span class="string">"rel"</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">b</span> <span class="attr">style</span>=<span class="string">"width:64px;left:-64px"</span>&gt;</span> </span><br><span class="line">       <span class="tag">&lt;<span class="name">i</span> <span class="attr">style</span>=<span class="string">"width: 16px;"</span>&gt;</span>8<span class="tag">&lt;/<span class="name">i</span>&gt;</span> </span><br><span class="line">       <span class="tag">&lt;<span class="name">i</span> <span class="attr">style</span>=<span class="string">"width: 16px;"</span>&gt;</span>3<span class="tag">&lt;/<span class="name">i</span>&gt;</span> </span><br><span class="line">       <span class="tag">&lt;<span class="name">i</span> <span class="attr">style</span>=<span class="string">"width: 16px;"</span>&gt;</span>9<span class="tag">&lt;/<span class="name">i</span>&gt;</span> </span><br><span class="line">       <span class="tag">&lt;<span class="name">i</span> <span class="attr">style</span>=<span class="string">"width: 16px;"</span>&gt;</span>5<span class="tag">&lt;/<span class="name">i</span>&gt;</span></span><br><span class="line">   <span class="tag">&lt;/<span class="name">b</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">b</span> <span class="attr">style</span>=<span class="string">"width: 16px;left:-32px"</span>&gt;</span>0<span class="tag">&lt;/<span class="name">b</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">b</span> <span class="attr">style</span>=<span class="string">"width: 16px;left:-48px"</span>&gt;</span>7<span class="tag">&lt;/<span class="name">b</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">b</span> <span class="attr">style</span>=<span class="string">"width: 16px;left:-16px"</span>&gt;</span>5<span class="tag">&lt;/<span class="name">b</span>&gt;</span> </span><br><span class="line"><span class="tag">&lt;/<span class="name">em</span>&gt;</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>覆盖操作是根据由偏移量计算得出的下标进行的，实际上就是列表元素的替换。当基准数据列表的元素数量超过包裹着 i 标签的 b 标签宽度时，我们就对列表进行切片，否则按照原来的替换规则进行。因此，需要对代码做一些调整。调整内容如下：</p>
                  <figure class="highlight vala">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="meta"># 减号代表删除此行代码，加号代表新增代码</span></span><br><span class="line">+ import re </span><br><span class="line">- base_price = b1.css(<span class="string">'i::text'</span>).extract() </span><br><span class="line">+ b1_style = b1.css(<span class="string">'b::attr("style")'</span>).<span class="keyword">get</span>() </span><br><span class="line"><span class="meta"># 获得具体的位置</span></span><br><span class="line">+ b1_width = <span class="string">''</span>.join(re.findall(<span class="string">'width:(.*)px;'</span>, b1_style)) </span><br><span class="line">+ number = <span class="keyword">int</span>(<span class="keyword">int</span>(b1_width) / <span class="number">16</span>) </span><br><span class="line"><span class="meta"># 获取第 1 对 &lt;b&gt; 标签中的值(列表) </span></span><br><span class="line">+ base_price = b1.css(<span class="string">'i::text'</span>).extract()[:number]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>如果列表中元素的数量超过标签宽度，那么后面的元素是不会显示的。比如 width:32px，每个标签占位宽度 16 px，那么即使 b 标签下有 5 个 i 标签（base_price=[1, 2 ,3 ,4 , 5]），在页面中也仅显示前面的两个数字。代码调整完毕后，再次运行代码。运行结果为：</p>
                  <figure class="highlight scheme">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[<span class="symbol">'4</span>', <span class="symbol">'6</span>', <span class="symbol">'7</span>'] </span><br><span class="line">[<span class="symbol">'8</span>', <span class="symbol">'7</span>', <span class="symbol">'0</span>', <span class="symbol">'5</span>']</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>第二架航班的票价结果仍然跟页面显示的内容不同，但根据 CSS 宽度规则，我们之前分析的逻辑是正确的。为什么结果还是跟页面显示的不一样呢？ 实际上并不是我们的逻辑和代码有错，而是页面显示错误。要注意的是，页面数据显示错误是常发生的事，我们只需要按照正确的逻辑编写代码即可。</p>
                  <h3 id="6-2-2-去哪儿网反爬虫案例"><a href="#6-2-2-去哪儿网反爬虫案例" class="headerlink" title="6.2.2 去哪儿网反爬虫案例"></a>6.2.2 去哪儿网反爬虫案例</h3>
                  <p>去哪儿网是中国领先的在线旅游平台，覆盖全球 68 万余条航线，并与国内的旅游景点和航空公司进行了深度的合作。去哪儿网也有用到类似的反爬虫手段，我们一起来了解一下。 打开浏览器并访问 <a href="https://dwz.cn/d05zNKyq，页面内容如图" target="_blank" rel="noopener">https://dwz.cn/d05zNKyq，页面内容如图</a> 6-13 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225224432.jpg" alt=""> 图 6-13 去哪儿网航班信息 航班票价对应的 HTML 代码如图 6-14 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225224514.jpg" alt=""> 图 6-14 去哪儿网航班票价 HTML 代码 去哪儿网航班票价所对应的 HTML 代码结构和 CSS 与我们在示例 5 中见到的类似。我们可以大胆猜测，去哪儿网航班票价的显示规律与示例 5 中所用的方法也是类似的，感兴趣的同学可以按照 6.2.1 节的思路进行票价推算。去哪儿网航班票价中第 1 对 b 标签下的 i 标签数量与 width 是相匹配的，并未出现显示错误的问题。</p>
                  <h3 id="6-2-3-小结"><a href="#6-2-3-小结" class="headerlink" title="6.2.3 小结"></a>6.2.3 小结</h3>
                  <p>CSS 样式可以改变页面显示，但这种“改变”仅存在于浏览器（能够解释 CSS 的渲染工具）中，即使爬虫工程师借助渲染工具，也无法获得“见到”的内容。 </p>
                  <h2 id="新书福利"><a href="#新书福利" class="headerlink" title="新书福利"></a>新书福利</h2>
                  <p>真是翘首以盼！《Python3 反爬虫原理与绕过实战》一书终于要跟大家见面了！为了感谢大家对韦世东和本书的期待与支持，在新书发布时会举办多场送书活动和限时折扣活动。 <img src="http://can.sfhfpc.com/sfhfpc/20191226081009.jpg" alt=""> 想要与作者韦世东交流或者参加新书发布活动的朋友可以扫描二维码进群与我互动哦！</p>
                  <h3 id="转载说明"><a href="#转载说明" class="headerlink" title="转载说明"></a>转载说明</h3>
                  <p>本篇内容摘自出版图书《Python3 反爬虫原理与绕过实战》，欢迎各位好友与同行转载！ 记得带上相关的版权信息哦😊。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/韦世东学算法和反爬虫" class="author" itemprop="url" rel="index">韦世东学算法和反爬虫</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-31 16:41:19" itemprop="dateCreated datePublished" datetime="2019-12-31T16:41:19+08:00">2019-12-31</time>
                </span>
                <span id="/8678.html" class="post-meta-item leancloud_visitors" data-flag-title="揭秘去哪儿网在用的 CSS 偏移反爬虫手段！" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>5.3k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>5 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8648.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8648.html" class="post-title-link" itemprop="url">大厂在用的反爬虫手段，破了它！</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>内容选自<strong>即将出版</strong>的《Python3 反爬虫原理与绕过实战》，本次公开书稿范围为第 6 章——文本混淆反爬虫。本篇为第 6 章中的第 3 小节，第 4 小节<a href="https://juejin.im/post/5e03ef93518825125c4316c3" target="_blank" rel="noopener"><strong>字体反爬虫</strong></a>已发布，其余小节将<strong>逐步放送</strong>。</p>
                  <h2 id="新书福利"><a href="#新书福利" class="headerlink" title="新书福利"></a>新书福利</h2>
                  <p>真是翘首以盼！《Python3 反爬虫原理与绕过实战》一书终于要跟大家见面了！为了感谢大家对韦世东和本书的期待与支持，在新书发布时会举办多场送书活动和限时折扣活动。</p>
                  <p><img src="http://can.sfhfpc.com/sfhfpc/20191226081009.jpg" alt="" title="null"></p>
                  <p>想要与作者韦世东交流或者参加新书发布活动的朋友可以扫描二维码进群与我互动哦！</p>
                  <h2 id="SVG-映射反爬虫"><a href="#SVG-映射反爬虫" class="headerlink" title="SVG 映射反爬虫"></a>SVG 映射反爬虫</h2>
                  <p>SVG 是用于描述二维矢量图形的一种图形格式。它基于 XML 描述图形，对图形进行放大或缩小操作都不会影响图形质量。矢量图形的这个特点使得它被广泛应用在 Web 网站中。 接下来我们要了解的反爬虫手段正是利用 SVG 实现的，这种反爬虫手段用矢量图形代替具体的文字，不会影响用户正常阅读，但爬虫程序却无法像读取文字那样获得 SVG 图形中的内容。由于 SVG 中的图形代表的也是一个个文字，所以在使用时必须在后端或前端将真实的文字与对应的 SVG 图形进行映射和替换，这种反爬虫手段被称为 SVG 映射反爬虫。</p>
                  <h3 id="6-3-1-SVG-映射反爬虫绕过实战"><a href="#6-3-1-SVG-映射反爬虫绕过实战" class="headerlink" title="6.3.1 SVG 映射反爬虫绕过实战"></a>6.3.1 SVG 映射反爬虫绕过实战</h3>
                  <p>示例 6：SVG 映射反爬虫示例。 网址：<a href="http://www.porters.vip/confusion/food.html" target="_blank" rel="noopener">http://www.porters.vip/confusion/food.html</a>。 任务：爬取美食商家评价网站页面中的商家联系电话、店铺地址和评分数据，页面内容如图 6-15 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225224848.jpg" alt=""> 图 6-15 示例 6 页面 在编写 Python 代码之前，我们需要确定目标数据的元素定位。在定位过程中，发现一个与以往不同的现象：有些数字在 HTML 代码中并不存在。例如口味的评分数据，其元素定位如图 6-16 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225224930.jpg" alt=""> 图 6-16 评分数据中口味分数元素定位 根据页面显示内容，HTML 代码中应该是 8.7 才对，但实际上我们看到的却是：</p>
                  <figure class="highlight xml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="tag">&lt;<span class="name">span</span> <span class="attr">class</span>=<span class="string">"item"</span>&gt;</span>口味:<span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhkjj4"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span>.7<span class="tag">&lt;/<span class="name">span</span>&gt;</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>HTML 代码中有数字 7 和小数点，但没有 8 这个数字，似乎数字 8 的位置被 d 标签占据。而商家电话号码处的显示就更奇怪了，一个数字都没有。商家电话对应的 HTML 代码如下：</p>
                  <figure class="highlight xml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"col more"</span>&gt;</span> </span><br><span class="line">        电话：</span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhkbvu"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk08k"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk08k"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">""</span>&gt;</span>-<span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk84t"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk6zl"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhkqsc"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhkqsc"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk6zl"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>包含很多的 d 标签，难道它使用 d 标签进行占位，然后用元素进行覆盖吗？我们可以将 d 标签的数量和数字的数量进行对比，发现它们的数量是相同的，也就是说一对 d 标签代表一个数字。 每一对 d 标签都有 class 属性，有些 class 属性值是相同的，有些则不同。我们再将 class 属性值与数字进行对比，看一看能否找到规律，如图 6-17 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225225103.jpg" alt=""> 图 6-17 class 属性值和数字的对比 从图 6-17 中可以看出，class 属性值和数字是一一对应的，如属性值 vhk08k 与数字 0 对应。根据这个线索，我们可以猜测每个数字都与一个属性值对应，对应关系如图 6-18 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225225143.jpg" alt=""> 图 6-18 数字与属性值对应关系 浏览器在渲染页面的时候就会按照这个对应关系进行映射，所以页面中显示的是数字，而我们在 HTML 代码中看到的则是这些 class 属性值。浏览器在渲染时将 HTML 中的 d 标签与数字按照此关系进行映射，并将映射结果呈现在页面中。映射逻辑如图 6-19 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225225239.jpg" alt=""> 图 6-19 映射逻辑 我们的爬虫代码可以按照同样的逻辑实现映射功能，在解析 HTML 代码时将 d 标签的 class 属性值取出来，然后进行映射即可得到页面中显示的数字。如何在爬虫代码中实现映射关系呢？实际上网页中使用的是“属性名数字”这种结构，Python 中内置的字典正好可以满足我们的需求。我们可以用 Python 代码测试一下，代码如下：</p>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># 定义映射关系</span></span><br><span class="line"><span class="string">mappings</span> <span class="string">=</span> <span class="string">&#123;'vhk08k':</span> <span class="number">0</span><span class="string">,</span> <span class="attr">'vhk6zl':</span> <span class="number">1</span><span class="string">,</span> <span class="attr">'vhk9or':</span> <span class="number">2</span><span class="string">,</span> </span><br><span class="line">   <span class="attr">'vhkfln':</span> <span class="number">3</span><span class="string">,</span> <span class="attr">'vhkbvu':</span> <span class="number">4</span><span class="string">,</span> <span class="attr">'vhk84t':</span> <span class="number">5</span><span class="string">,</span> </span><br><span class="line">   <span class="attr">'vhkvxd':</span> <span class="number">6</span><span class="string">,</span> <span class="attr">'vhkqsc':</span> <span class="number">7</span><span class="string">,</span> <span class="attr">'vhkjj4':</span> <span class="number">8</span><span class="string">,</span> </span><br><span class="line">   <span class="attr">'vhk0f1':</span> <span class="number">9</span><span class="string">&#125;</span> </span><br><span class="line"><span class="comment"># HTML 中得到的属性值</span></span><br><span class="line"><span class="string">html_d_class</span> <span class="string">=</span> <span class="string">'vhkvxd'</span> </span><br><span class="line"><span class="comment"># 将映射后的结果打印输出</span></span><br><span class="line"><span class="string">print(mappings.get(html_d_class))</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这段代码的逻辑是：首先定义属性值与数字的映射关系，然后假设一个 HTML 中 d 标签的属性值，接着将这个属性值的映射结果打印出来。代码运行后得到的结果为：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">6</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果说明映射这种方法是可行的。接着我们试一试将商家的联系电话映射出来：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># 定义映射关系</span></span><br><span class="line">mappings = &#123;<span class="string">'vhk08k'</span>: 0, <span class="string">'vhk6zl'</span>: 1, <span class="string">'vhk9or'</span>: 2, </span><br><span class="line">            <span class="string">'vhkfln'</span>: 3, <span class="string">'vhkbvu'</span>: 4, <span class="string">'vhk84t'</span>: 5, </span><br><span class="line">            <span class="string">'vhkvxd'</span>: 6, <span class="string">'vhkqsc'</span>: 7, <span class="string">'vhkjj4'</span>: 8, </span><br><span class="line">            <span class="string">'vhk0f1'</span>: 9&#125; </span><br><span class="line"><span class="comment"># 商家联系电话 class 属性</span></span><br><span class="line">html_d_class = [<span class="string">'vhkbvu'</span>, <span class="string">'vhk08k'</span>, <span class="string">'vhk08k'</span>, </span><br><span class="line">                <span class="string">''</span>, <span class="string">'vhk84t'</span>, <span class="string">'vhk6zl'</span>, </span><br><span class="line">                <span class="string">'vhkqsc'</span>, <span class="string">'vhkqsc'</span>, <span class="string">'vhk6zl'</span>] </span><br><span class="line"></span><br><span class="line">phone = [mappings.<span class="builtin-name">get</span>(i) <span class="keyword">for</span> i <span class="keyword">in</span> html_d_class] </span><br><span class="line"><span class="comment"># 将映射后的结果打印输出</span></span><br><span class="line"><span class="builtin-name">print</span>(phone)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果为：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[<span class="number">4</span>, <span class="number">0</span>, <span class="number">0</span>, None, <span class="number">5</span>, <span class="number">1</span>, <span class="number">7</span>, <span class="number">7</span>, <span class="number">1</span>]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们使用映射的方法得到了商家联系电话，说明 SVG 映射反爬虫已经被我们绕过了。</p>
                  <h3 id="6-3-2-大众点评反爬虫案例"><a href="#6-3-2-大众点评反爬虫案例" class="headerlink" title="6.3.2 大众点评反爬虫案例"></a>6.3.2 大众点评反爬虫案例</h3>
                  <p>这种映射手段不仅仅出现在本书的示例中，在大型网站中也有应用。大众点评是中国领先的本地生活信息及交易平台，也是全球最早建立的独立第三方消费点评网站。大众点评不仅为用户提供商户信息、消费点评及消费优惠等信息服务，同时提供团购、餐厅预订、外卖和电子会员卡等 O2O（Online To Offline）交易服务。大众点评网站也使用了映射型反爬虫手段，打开浏览器并访问 <a href="https://www.dianping.com/shop/14741057，页面如图" target="_blank" rel="noopener">https://www.dianping.com/shop/14741057，页面如图</a> 6-20 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225225522.jpg" alt=""> 图 6-20 大众点评商家信息页 大众点评的商家信息页主要用于展示消费者对商家的各项评分、商家电话、店铺地址和推荐菜品等。我们可以看一看商家电话或评分的 HTML 代码，如图 6-21 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225225608.jpg" alt=""> 图 6-21 商家电话 HTML 代码 大众点评中的商家号码并不是全部使用 d 标签代替，其中有部分使用了数字。但是仔细观察一下就可以发现商家号码的数量等于 d 标签数量加上数字的数量，说明 d 标签的 class 属性值与数字也有可能是一一对应的映射关系。感兴趣的同学可以使用示例 6 中的方法，尝试映射大众点评案例中的数字。 如果这种手段的绕过方法这么简单的话，那么它早就被淘汰了，为什么连大众点评这样的大型网站都会使用呢？我们继续往下看，大众点评的商家营业时间部分的 HTML 代码如图 6-22 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225225659.jpg" alt=""> 图 6-22 大众点评商家营业时间 除了刚才的数字映射之外，大众点评还对中文进行了映射。此时如果按照示例 6 中人为地将 class 值和对应的文字进行映射的话，就非常麻烦了。试想一下，如果网页中所有的文字都使用这种映射反爬虫的手段，那么爬虫工程师要如何应对呢？对所有用到的文字进行映射吗？ 这不可能做到，其中要完成映射的包括 10 个数字、26 个英文字母和几千个常用汉字。而且目标网站一旦更改文字的对应关系，那么爬虫工程师就需要重新映射所有文字。面对这样的问题，我们必须找到文字映射规律，并且能够使用 Python 语言实现映射算法。如此一来，无论目标网站文字映射的对应关系如何变化，我们都能够使用这套映射算法得到正确的结果。 这种映射关系在网页中是如何实现的呢？是使用 JavaScript 在页面中定义数组吗？还是异步请求API 拿到 JSON 数据？这都有可能，接下来我们就去寻找答案。</p>
                  <h3 id="6-3-3-SVG-反爬虫原理"><a href="#6-3-3-SVG-反爬虫原理" class="headerlink" title="6.3.3 SVG 反爬虫原理"></a>6.3.3 SVG 反爬虫原理</h3>
                  <p>映射关系不可能凭空出现，一定使用了某种技术特性。HTML 中与标签 class 属性相关的只有 JavaScript 和 CSS。根据这个线索，我们需要继续对示例 6 进行分析。案例中商家电话的 HTML 代码为：</p>
                  <figure class="highlight xml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="tag">&lt;<span class="name">div</span> <span class="attr">class</span>=<span class="string">"col more"</span>&gt;</span>电话：</span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhkbvu"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk08k"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk08k"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">""</span>&gt;</span>-<span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk84t"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk6zl"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhkqsc"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhkqsc"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line">   <span class="tag">&lt;<span class="name">d</span> <span class="attr">class</span>=<span class="string">"vhk6zl"</span>&gt;</span><span class="tag">&lt;/<span class="name">d</span>&gt;</span> </span><br><span class="line"><span class="tag">&lt;/<span class="name">div</span>&gt;</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们可以随意选择一对 d 标签，然后观察它对应的 CSS 样式有没有可以深入分析的线索，如果没有线索再看 JavaScript。 d 标签的 CSS 样式如下：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">d</span><span class="selector-attr">[class^=<span class="string">"vhk"</span>]</span> &#123; </span><br><span class="line">   <span class="attribute">width</span>: <span class="number">14px</span>; </span><br><span class="line">   <span class="attribute">height</span>: <span class="number">30px</span>; </span><br><span class="line">   <span class="attribute">margin-top</span>: -<span class="number">9px</span>; </span><br><span class="line">   <span class="attribute">background-image</span>: <span class="built_in">url</span>(../font/food.svg); </span><br><span class="line">   <span class="attribute">background-repeat</span>: no-repeat; </span><br><span class="line">   <span class="attribute">display</span>: inline-block; </span><br><span class="line">   <span class="attribute">vertical-align</span>: middle; </span><br><span class="line">   <span class="attribute">margin-left</span>: -<span class="number">6px</span>; </span><br><span class="line">&#125; </span><br><span class="line"><span class="selector-class">.vhkqsc</span> &#123; </span><br><span class="line">    <span class="attribute">background</span>: -<span class="number">288.0px</span> -<span class="number">141.0px</span>; </span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>d 标签样式看上去没有什么特别之处，只是设置了 background 属性的坐标值。但是上方 d 标签的公共样式中设置了背景图片，我们可以复制背景图片的地址，在浏览器的新标签页中打开，d 标签背景图如图 6-23 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225225857.jpg" alt=""> 图 6-23 标签背景图 d 标签的背景图中全部都是数字，这些无序的数字共有 4 行。但这好像不是一张大图片，我们查看该图片页面的源代码，内容如图 6-24 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225225953.jpg" alt=""> 图 6-24 图片页面源代码 源代码中前两行表明这是一个 SVG 文件，该文件中使用 text 标签定义文本， style 标签用于设置文本样式， text 标签定义的文本正是图片页面显示的数字。难道这些无序的数字就是我们在页面中看到的电话号码和评分数字？ 除了 class 属性值为 vhkbvu 的 d 标签，其他标签也使用了这个的 CSS 样式，但每对 d 标签的坐标定位都不同。它们的坐标定位如下：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-class">.vhkbvu</span> &#123; </span><br><span class="line">    <span class="attribute">background</span>: -<span class="number">386px</span> -<span class="number">97px</span>; </span><br><span class="line">&#125; </span><br><span class="line"><span class="selector-class">.vhk08k</span> &#123; </span><br><span class="line">    <span class="attribute">background</span>: -<span class="number">274px</span> -<span class="number">141px</span>; </span><br><span class="line">&#125; </span><br><span class="line"><span class="selector-class">.vhk84t</span> &#123;</span><br><span class="line">    <span class="attribute">background</span>: -<span class="number">176px</span> -<span class="number">141px</span>; </span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>坐标是定位数字的关键，要想知道坐标的计算方法，必须了解一些关于 SVG 的知识。 在本节开始的时候，我们简单地了解了 SVG 的概念，知道 SVG 是基于 XML 的。实际上它是用文本格式的描述性语言来描述图像内容的，因此 SVG 是一种与图像分辨率无关的矢量图形格式。打开文本编辑器，并在新建的文件中写入以下内容：</p>
                  <figure class="highlight django">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="xml"><span class="meta">&lt;?xml version="1.0" encoding="UTF-8" standalone="no"?&gt;</span> </span></span><br><span class="line"><span class="xml"><span class="meta">&lt;!DOCTYPE <span class="meta-keyword">svg</span> <span class="meta-keyword">PUBLIC</span> <span class="meta-string">"-//W3C//DTD SVG 1.1//EN"</span> <span class="meta-string">"http://www.w3.org/Graphics/SVG/1.1/ </span></span></span></span><br><span class="line"><span class="xml"> DTD/svg11.dtd"&gt; </span></span><br><span class="line"><span class="xml"><span class="tag">&lt;<span class="name">svg</span> <span class="attr">xmlns</span>=<span class="string">"http://www.w3.org/2000/svg"</span> <span class="attr">version</span>=<span class="string">"1.1"</span> <span class="attr">xmlns:xlink</span>=<span class="string">"http://www.w3.org/ </span></span></span></span><br><span class="line"><span class="xml"> 1999/xlink" width="250px" height="250.0px"&gt; </span></span><br><span class="line"><span class="xml">    <span class="tag">&lt;<span class="name">text</span> <span class="attr">x</span>=<span class="string">'10'</span> <span class="attr">y</span>=<span class="string">'30'</span>&gt;</span>hello,world<span class="tag">&lt;/<span class="name">text</span>&gt;</span> </span></span><br><span class="line"><span class="xml"><span class="tag">&lt;/<span class="name">svg</span>&gt;</span></span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>将该文件保存为 test.svg，然后使用浏览器打开 test.svg 文件，显示内容如图 6-25 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225230140.jpg" alt=""> 图 6-25 test.svg 显示内容 代码前 3 行声明文件类型，第 4 行~第 5 行定义了 SVG 内容块和画布宽高，第 6 行使用 text 标签定义了一段文本并指定了文本的坐标。这段文本就是我们在浏览器中看到的内容，而代码中的 <em>x</em> 坐标和 <em>y</em> 坐标则用于确定该文本在画布中的位置，坐标规则如下。</p>
                  <ul>
                    <li>以页面的左上角为零坐标点，即坐标值为 (0, 0)。</li>
                    <li>坐标以像素为单位。</li>
                    <li><em>x</em> 轴的正方向为从左到右，<em>y</em> 轴的正方向是从上到下。</li>
                    <li><em>n</em> 个字符可以有 <em>n</em> 个位置参数。</li>
                  </ul>
                  <p>如果字符数量大于位置参数数量，那么没有位置参数的字符将以最后一个位置参数为零坐标点，并按原文顺序排列。 看上去并不是很好理解，我们可以通过修改代码来理解坐标轴的定义。首先是 <em>x</em> 轴， text 标签中的 <em>x</em> 代表列表字符在页面中的 <em>x</em> 轴位置，test.svg 中的 <em>x</em> 值为 10，现在我们将其设为 0 ，保存后刷新网页，页面内容如图 6-26 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225230255.jpg" alt=""> 图 6-26 <em>x</em> 为 0 时的 test.svg 显示内容 <em>x</em> 的值为 0 时，文本紧贴浏览器左侧。而 <em>x</em> 的值为 10 时，文本距离浏览器左侧有一定的距离，这说明 <em>x</em> 的值能够决定文字所在的位置。现在我们将代码中 <em>x</em> 对应的值改为“10 50 30 40 20 60”（注意这里特意将第 2个数字 20与第 5个数字互换了位置），这样做是为了设定前 6个字符的坐标位置。 此时，第 1 个字符的位置参数为 10，第 2 个字符的位置参数为 50，第 3 个字符的位置参数为 30，以此类推，页面中正常显示的文字顺序应该是：</p>
                  <figure class="highlight autohotkey">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="built_in">holle,</span>world</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>但是由于我们调换了第 2 个字符和第 5 个字符的位置参数，即字母 e 和字母 o 的位置互换，如图 6-27 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225230403.jpg" alt=""> 图 6-27 设定多个 <em>x</em> 值的 svg 图 6-27 中文字顺序与我们猜测的顺序是一样的，这说明 SVG 中每个字符都可以有自己的 <em>x</em> 轴坐标值。<em>y</em> 与 <em>x</em> 同理，每个字符都可以有自己的 <em>y</em> 轴坐标值。虽然我们只设定了 6 个位置参数， svg 中的字符却有 11 个，但没有设定位置参数的字符依然能够按照原文顺序排序。在了解 SVG 基本知识之后，我们回头看一下案例中所使用的 SVG 文件中坐标参数的设定，图 6-23 中的字符与图 6-24 图片页源代码中的字符一一对应，且每个字符都设定了 <em>x</em> 轴的位置参数，而 <em>y</em> 轴则只有 1 个值。 在了解位置参数之后，我们还需要弄清楚字符定位的问题。浏览器根据 CSS 样式中设定的坐标和元素宽高来确定 SVG 中对应数字。<em>x</em> 轴的正方向为从左到右，<em>y</em> 轴的正方向是从上到下，如图 6-28 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225230507.jpg" alt=""> 图 6-28 SVG <em>x</em> 轴和 <em>y</em> 轴与位置参数的关系 而 CSS 样式中的 <em>x</em> 轴与 <em>y</em> 轴是相反的，也就是说 CSS 样式中 <em>x</em> 轴是负数向右的，<em>y</em> 轴是负数向下的，如图 6-29 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225230553.jpg" alt=""> 图 6-29 CSS <em>x</em> 轴和 <em>y</em> 轴与位置参数的关系 所以当我们需要在 CSS 中定位 SVG 中的字符位置时，需要用负数表示。我们可以通过一个例子来理解它们的关系，现在需要在 CSS 中定位图 6-30 中第 1 行的第 1 个字符的中心点。 <img src="http://can.sfhfpc.com/sfhfpc/20191225230642.jpg" alt=""> 图 6-30 SVG 假设字符大小为 14 px，那么 SVG 的计算规则如下。</p>
                  <ul>
                    <li>字符在<em>x</em>轴中心点的计算规则为：字符大小除以2，再加字符的<em>x</em>轴起点位置参数，即14÷2+0 等于 7。</li>
                    <li>字符在 <em>y</em> 轴中心点的计算规则为：<em>y</em> 轴高度减字符 <em>y</em> 轴起点减字符大小，其值除以 2 后加上字符 <em>y</em> 轴起点位置参数，最后再加上字符大小数值的一半，即(38−0−14)÷2+0+7 等于 19。</li>
                  </ul>
                  <p>最后得到 SVG 的坐标为：</p>
                  <figure class="highlight gml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="symbol">x</span>=<span class="string">'7'</span> <span class="symbol">y</span>=<span class="string">'19'</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>CSS 样式的 <em>x</em> 轴和 <em>y</em> 轴与 SVG 是相反的，所以 CSS 样式中对该字符的定位为：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">\<span class="number">-7</span>px <span class="number">-19</span>px</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样就能够定位到指定字符的中心点了。但是如果要在 HTML 页面中完整显示该字符，那么还需要为 HTML 中对应的标签设置宽高样式，如：</p>
                  <figure class="highlight scss">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">width</span>: <span class="number">14px</span>; </span><br><span class="line"><span class="attribute">height</span>: <span class="number">30px</span>;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在了解了 SVG 与 CSS 样式的关联关系后，我们就能够根据 CSS 样式映射出 SVG 中对应的字符。 在实际场景中，我们需要让程序能够自动处理 CSS 样式和 SVG 的映射关系，而不是人为地完成这些 工作。以示例 6 中的 SVG 和 CSS 样式为例，假如我们需要用 Python 代码实现自动映射功能，首先我 们就需要拿到这两个文件的 URL，如：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">url_css</span> = <span class="string">'http://www.porters.vip/confusion/css/food.css'</span> </span><br><span class="line"><span class="attr">url_svg</span> = <span class="string">'http://www.porters.vip/confusion/font/food.svg'</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>还有需要映射的 HTML 标签的 class 属性值，如：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">css_class_name</span> = <span class="string">'vhkbvu'</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>接下来使用 Requests 库向 URL 发出请求，拿到文本内容。对应代码如下：</p>
                  <figure class="highlight arduino">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">import</span> requests </span><br><span class="line">css_resp = requests.<span class="built_in">get</span>(url_css).<span class="built_in">text</span> </span><br><span class="line">svg_resp = requests.<span class="built_in">get</span>(url_svg).<span class="built_in">text</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>提取 CSS 样式文件中标签属性对应的坐标值，这里使用正则进行匹配即可。对应代码如下：</p>
                  <figure class="highlight sas">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import re </span><br><span class="line">pile = <span class="string">'.%s&#123;background:-(d+)px-(d+)px;&#125;'</span> % css_class_name </span><br><span class="line">pattern = re.compile(pile) </span><br><span class="line">css = css_resp.<span class="meta">replace</span>(<span class="string">'n'</span>, <span class="string">''</span>).<span class="meta">replace</span>(<span class="string">' '</span>, <span class="string">''</span>) </span><br><span class="line">coord = pattern.findall(css) </span><br><span class="line"><span class="meta">if</span> coord: </span><br><span class="line"> <span class="meta">x</span>, y = coord[0] </span><br><span class="line"> <span class="meta">x</span>, y =<span class="meta"> int(</span><span class="meta">x</span>),<span class="meta"> int(</span>y)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>此时得到的坐标值是正数，可以直接用于 SVG 字符定位。定位前我们要先拿到 SVG 中所有 text 标签的 Element 对象：</p>
                  <figure class="highlight oxygene">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> parsel import <span class="keyword">Selector</span> </span><br><span class="line">svg_data = <span class="keyword">Selector</span>(svg_resp) </span><br><span class="line">texts = svg_data.xpath(<span class="string">'//text'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>然后获取所有 text 标签中的 y 值，接着我们将上一步得到的 Element 对象进行循环取值即可：</p>
                  <figure class="highlight markdown">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">axis_y = [<span class="string">i.attrib.get('y') for i in texts if y &lt;= int(i.attrib.get('y'))</span>][<span class="symbol">0</span>]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>得到 <em>y</em> 值后就可以开始字符定位了。要注意的是，SVG 中 text 标签的 <em>y</em> 值与 CSS 样式中得到的 <em>y</em> 值并不需要完全相等，因为样式可以随意调整，比如 CSS 样式中-90 和-92 对于 SVG 的定位来说并没有什么差别，所以我们只需要知道具体是哪一个 text 即可。 那么如何确定是哪一个 text呢？ 我们可以用排除法来确定，假如当前 CSS 样式中的 <em>y</em> 值是-97，那么在 SVG 中 text 的 <em>y</em> 值就不可能小于 97，我们只需要取到比 97 大且最相近的 text 标签 <em>y</em> 值即可。比如当前 SVG 所有 text 标签的 <em>y</em> 值为：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[<span class="number">38</span>, <span class="number">83</span>, <span class="number">120</span>, <span class="number">164</span>]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>那么大于 97 且最相近的是 120。将这个逻辑转化为代码：</p>
                  <figure class="highlight markdown">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">axis_y = [<span class="string">i.attrib.get('y') for i in texts if y &lt;= int(i.attrib.get('y'))</span>][<span class="symbol">0</span>]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>得到 y 值后就可以确定具体是哪个 text 标签了。对应代码如下：</p>
                  <figure class="highlight excel">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">svg_text = svg_data.xpath('//<span class="built_in">text</span>[@y=<span class="string">"%s"</span>]/<span class="built_in">text</span>()' % axis_y).extract_first()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>接下来需要确认 SVG 中的文字大小，也就是需要找到 font-size 属性的值。对应代码如下：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">font_size</span> = re.search(<span class="string">'font-size:(d+)px'</span>, svg_resp).group(<span class="number">1</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>得到 font-size 的值后，我们就可以定位具体的字符了。<em>x</em> 轴有多少个字符呢？刚才我们拿到的 svg_text 就是指定的 text 标签中的字符：</p>
                  <figure class="highlight 1c">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">'<span class="number">67126078110409</span><span class="number">66630008923284</span><span class="number">40489239185923</span>'</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>我们需要计算字符串长度吗？并不用，我们知道，每个字符大小为 14 px，只需要将 CSS 样式中的 <em>x</em> 值除以字符大小，得到的就是该字符在字符串中的位置。除法得到的结果有可能是整数也有可能是非整数，当结果是整数是说明定位完全准确，我们利用切片特性就可以拿到字符。如果结果是非整数，就说明定位不完全准确，由于字符不可能出现一半，所以我们利用地板除（编程语言中常见的向下取整除法，返回商的整数部分。）就可以拿到整数：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">position</span> = x // int(font_size) <span class="comment"># 结果为 27</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>也就是说 CSS 样式 vhkbvu 映射的是 SVG 中第 4 行文本的第 27 个位置的值。映射结果如图 6-31 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191225231136.jpg" alt=""> 图 6-31 映射结果 然后再利用切片特性拿到字符。对应代码如下：</p>
                  <figure class="highlight fortran">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">number</span> = svg_text[<span class="keyword">position</span>] </span><br><span class="line"><span class="built_in">print</span>(<span class="keyword">number</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>代码运行结果为 4。我们还可以尝试其他的 class 属性值，最后得到的结果与页面显示的字符都是相同的，说明这种映射算法是正确的。至此，我们已经完成了对映射型反爬虫的绕过。</p>
                  <h3 id="6-3-4-小结"><a href="#6-3-4-小结" class="headerlink" title="6.3.4 小结"></a>6.3.4 小结</h3>
                  <p>与 6.1 节和 6.2 节相同，本节示例所用的反爬虫手段，即使借助渲染工具也无法获得“见到”的内容。SVG 映射反爬虫利用了浏览器与编程语言在渲染方面的差异，以及 SVG 与 CSS 定位这样的前端知识。如果爬虫工程师不熟悉渲染原理和前端知识，那么这种反爬虫手段就会带来很大的困扰。 </p>
                  <h3 id="转载说明"><a href="#转载说明" class="headerlink" title="转载说明"></a>转载说明</h3>
                  <p>本篇内容摘自出版图书《Python3 反爬虫原理与绕过实战》，欢迎各位好友与同行转载！ 记得带上相关的版权信息哦😊。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/韦世东学算法和反爬虫" class="author" itemprop="url" rel="index">韦世东学算法和反爬虫</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-27 15:09:11" itemprop="dateCreated datePublished" datetime="2019-12-27T15:09:11+08:00">2019-12-27</time>
                </span>
                <span id="/8648.html" class="post-meta-item leancloud_visitors" data-flag-title="大厂在用的反爬虫手段，破了它！" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>9.1k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>8 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8637.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8637.html" class="post-title-link" itemprop="url">谷歌验证码 ReCaptcha 破解教程，简单方便从零开始。</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>很久没有做爬虫破解类相关的分享了，之前交流群里有朋友提问谷歌系的reCAPTCHA V2 验证码怎么破，因为工作的原因我是很久之后才看到的，也不知道那位朋友后来成功了没有。所以今天就来跟大家分享一下 reCAPTCHA V2 的破解。 （小马补充：想加交流群的朋友，进入公众号下方，点击技术交流，有读者群和交流群，点击后都会弹出崔老师的二维码，扫微信二维码拉群～） 如果大家访问国外的一些网站的话，想必肯定见过这样的一个验证码，它上面写着「I’m not a robot」，意为「我不是机器人」，验证码长这个样子： <img src="https://qiniu.cuiqingcai.com/2019-12-26-001513.png" alt=""> 这时候，只要我们点击最前面的复选框，验证码算法会首先利用其「风险分析引擎」做一次安全检测，如果直接检验通过的话，我们会直接得到如下的结果： <img src="https://qiniu.cuiqingcai.com/2019-12-26-000950.png" alt=""> 如果算法检测到当前系统存在风险，比如可能是陌生的网络环境，可能是模拟程序，会需要做二次校验。它会进一步弹出类似如下的内容： <img src="https://qiniu.cuiqingcai.com/2019-12-26-002143.png" alt=""> 比如上面这张图，验证码页面会出现九张图片，同时最上方出现文字「树木」，我们需要点选下方九张图中出现「树木」的图片，点选完成之后，可能还会出现几张新的图片，我们需要再次完成点选，最后点击「验证」按钮即可完成验证。 或者我们可以点击下方的「耳机」图标，这时候会切换到听写模式，验证码会变成这样： <img src="https://qiniu.cuiqingcai.com/2019-12-26-004236.png" alt=""> 这时候我们如果能填写对验证码读的音频内容，同样可以通过验证。 这两种方式都可以通过验证，验证完成之后，我们才能完成表单的提交，比如完成登录、注册等操作。 这种验证码叫什么名字？ 这个验证码就是 Google 的 reCAPTCHA V2 验证码，它就属于行为验证码的一种，这些行为包括点选复选框、选择对应图片、语音听写等内容，只有将这些行为校验通过，此验证码才能通过验证。相比于一般的图形验证码来说，此种验证码交互体验更好、安全性会更高、破解难度更大。 许多国外的网站都采用了此种验证码，由于某些原因，在国内其实无法直接使用，但只需要将验证码的域名更换为 recaptcha.net 同样是可以使用的，所以有时候我们在国内某些站点同样能看到它的身影。 其实上文所介绍的验证码仅仅是 reCAPTCHA 验证码的一种形式，是 V2 的显式版本，另外其 V2 版本还有隐式版本，隐式版本在校验的时候不会再显式地出现验证页面，它是通过 JavaScript 将验证码和提交按钮进行绑定，在提交表单的时候会自动完成校验。除了 V2 版本，Google 又推出了最新的 V3 版本，reCAPTCHA V3 验证码会为根据用户的行为来计算一个分数，这个分数代表了用户可能为机器人的概率，最后通过概率来判断校验是否可以通过。其安全性更高、体验更好。 具体的内容大家可以参考 reCAPTCHA 的官方介绍：<a href="https://developers.google.com/recaptcha" target="_blank" rel="noopener">https://developers.google.com/recaptcha</a>。 那么在做爬虫的时候，如果我们遇到了这样的验证码？该怎么办呢？不要着急，这篇文章就来介绍一个解决方案。</p>
                  <h2 id="机器学习-vs-识别服务"><a href="#机器学习-vs-识别服务" class="headerlink" title="机器学习 vs 识别服务"></a>机器学习 vs 识别服务</h2>
                  <p>之前我在写上一篇如何识别滑动验证码问题的时候，当时朋友留言问我能不能做一个机器学习的，我回复了，我说当然没问题，你等着，我这周就做。 我那周从周一做到周五，我记得用的应该是 yolo，反复修改，小马还经常过来催崔稿，耗费良久，然后就在那周周五晚上的23:59分，我灵机一动，终于明白了。 去他的机器学习，有服务不好吗？ reCAPTCHA 本身比极验还要复杂，国内网站我暂时没看到破解的，然后这次是用的俄罗斯的一个服务商 2Captcha 提供的 图像识别和一系列行为验证码的识别服务。 <img src="https://qiniu.cuiqingcai.com/2019-12-26-012846.png" alt=""> 破解验证码背后有图像识别算法和大量人力的支撑，如果我们仅仅是简单的图形验证码，其可以通过一些图像识别算法将内容识别出来转化为文本内容。如果是较为复杂的图形验证码或者像 reCAPTCHA 类似的行为验证码，其背后会有人来对验证码进行模拟，然后返回其验证成功后的秘钥，我们利用其结果便可以完成一些验证码的绕过。 当然这种网站肯定是要收费的，按照 1000 次识别为单位，其花费的费用为 0.5 美刀到 2.99 美刀不等，比如非常简单的图形验证码可能就是 0.5 美刀，这种验证码对于其人力和算力资源消耗都是相对较小的，对于复杂的 reCAPTCHA 验证码，就要花 2.99 美刀了，因为识别这么一个验证码并不容易，其背后的人可能需要看好多图，点选好多次才能完成一次成功的验证。 目前我用的服务的收费标准是这样的： <img src="https://qiniu.cuiqingcai.com/2019-12-26-012237.png" alt=""> 具体的内容或者更新大家可以到其官方说明 <a href="https://2captcha.com/2captcha-api#rates" target="_blank" rel="noopener">https://2captcha.com/2captcha-api#rates</a> 去查看。 后面我用他的服务来破解 reCAPTCHA，当然类比其他服务也是可以的，过程大概都是这样。</p>
                  <h2 id="准备工作"><a href="#准备工作" class="headerlink" title="准备工作"></a>准备工作</h2>
                  <p>要使用 2Captcha，第一步当然是注册一下它的账号了，注册完成之后我们可以进入到 2Captcha 的控制台，类似于这样子： <img src="https://qiniu.cuiqingcai.com/2019-12-26-013234.png" alt=""> 在这里我们可以看到账户余额、API KEY、FAQ 等配置。 这里最重要的就是 API KEY 了，它是我们用来使用 2Captcha 的凭证，我们将它复制下来，后面我们会在代码中使用它。 <img src="https://qiniu.cuiqingcai.com/2019-12-26-013456.png" alt=""> 好，准备工作完成了，我们接下来进入正式内容。</p>
                  <h2 id="2Captcha-for-reCAPTCHA-V2"><a href="#2Captcha-for-reCAPTCHA-V2" class="headerlink" title="2Captcha for reCAPTCHA V2"></a>2Captcha for reCAPTCHA V2</h2>
                  <p>在上文我们已经介绍过 reCAPTCHA V2 的使用和交互流程了，下面我们来介绍下其识别和绕过的基本流程。 在这里我们就拿官方的 reCAPTCHA V2 的示例网站来做演示吧，其网址为：<a href="https://www.google.com/recaptcha/api2/demo，打开之后界面如下所示" target="_blank" rel="noopener">https://www.google.com/recaptcha/api2/demo，打开之后界面如下所示</a>： <img src="https://qiniu.cuiqingcai.com/2019-12-26-014533.png" alt=""> 在这里可以看到有一个表单，上面有一些输入框，下方是 reCAPTCHA V2 验证码。 要识别这个验证码，第一步便是找到这个验证码 sitekey，这个是验证码的唯一标识。 我们打开浏览器的开发者工具，查看其页面源码，首先找到 reCAPTCHA 的源代码，如下图所示： <img src="https://qiniu.cuiqingcai.com/2019-12-26-015303.png" alt=""> 可以看到 reCAPTCHA 是对应了一个 iframe，我们看到的 reCAPTCHA 内容都是在 iframe 里面呈现出来的。 这里我们可以观察到在 reCAPTCHA 的源码的最外层的 div 上面有一个字段，叫做 data-sitekey，这就是刚才我们所说的 sitekey，它是验证码的唯一标识，比如这里我先将这个 sitekey 保存下来，这里其值为：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="number">6</span>Le-wvkSAAAAAPBMRTvw0Q4Muexq9bi0DJwx_mJ-</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>下一步，我们就需要将这个 sitekey 和当前页面的 URL 告诉 2Captcha，让 2Captcha 来帮助我们识别这个 reCAPTCHA 验证码，告诉 2Captcha 之后，2Captcha 会利用这些信息加载出对应的验证码，再利用其背后的人力来对验证码进行识别，最后将识别得到的 token 返回给我们即可。 好，那么接下来怎么把这个信息告诉 2Captcha 呢？ 很简单，2Captcha 为我们提供了一个接口，其接口地址为：<a href="https://2captcha.com/in.php，我们只需要将对应的信息发送到这个接口就好了" target="_blank" rel="noopener">https://2captcha.com/in.php，我们只需要将对应的信息发送到这个接口就好了</a>。 那么发送需要什么参数呢，在这里介绍一下：</p>
                  <p>参数</p>
                  <p>类型</p>
                  <p>必须</p>
                  <p><strong>描述</strong></p>
                  <p>key</p>
                  <p>String</p>
                  <p>Yes</p>
                  <p>我们自己的 API KEY</p>
                  <p>method</p>
                  <p>String</p>
                  <p>Yes</p>
                  <p>userrecaptcha，定义破解 reCAPTCHA 验证码的方式</p>
                  <p>googlekey</p>
                  <p>String</p>
                  <p>Yes</p>
                  <p>reCAPTCHA 的 sitekey</p>
                  <p>pageurl</p>
                  <p>String</p>
                  <p>Yes</p>
                  <p>reCAPTCHA 当前所在的 URL</p>
                  <p>invisible</p>
                  <p>Integer Default: 0</p>
                  <p>No</p>
                  <p>是否可见，1 代表是隐式验证码，0 代表普通验证码。</p>
                  <p>header_acao</p>
                  <p>Integer Default: 0</p>
                  <p>No</p>
                  <p>跨域访问配置</p>
                  <p>pingback</p>
                  <p>String</p>
                  <p>No</p>
                  <p>回调地址</p>
                  <p>json</p>
                  <p>Integer Default: 0</p>
                  <p>No</p>
                  <p>返回格式，1 代表返回 JSON 格式，0 代表纯文本，默认 0</p>
                  <p>soft_id</p>
                  <p>Integer</p>
                  <p>No</p>
                  <p>ID of software developer. Developers who integrated their software with 2captcha get reward: 10% of spendings of their software users.</p>
                  <p>proxy</p>
                  <p>String</p>
                  <p>No</p>
                  <p>代理配置</p>
                  <p>在这里我们可以构造一个 URL，它包括这几个参数：</p>
                  <ul>
                    <li>key：注意这里的 KEY 换成你自己的 API KEY</li>
                    <li>method：直接赋值 userrecaptcha</li>
                    <li>googlekey：复制的 sitekey</li>
                    <li>pageurl：当前 URL</li>
                    <li>json：直接赋值 1，代表返回 JSON 格式</li>
                  </ul>
                  <p>比如在这里我就构造了这个 URL，内容如下：</p>
                  <figure class="highlight llvm">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">https://<span class="number">2</span>captcha.com/in.php?key=<span class="keyword">c</span><span class="number">0</span>ae<span class="number">5935</span>d<span class="number">807</span><span class="keyword">c</span><span class="number">28</span>f<span class="number">285e5</span>cb<span class="number">16</span><span class="keyword">c</span><span class="number">676</span>a<span class="number">48</span>&amp;method=userrecaptcha&amp;googlekey=<span class="number">6</span>Le-wvkSAAAAAPBMRTvw<span class="number">0</span>Q<span class="number">4</span>Muexq<span class="number">9</span>b<span class="keyword">i0</span>DJwx_mJ-&amp;pageurl=https://www.google.com/recaptcha/ap<span class="keyword">i2</span>/demo&amp;json=<span class="number">1</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这时候我们直接向这个 URL 发起一个 GET 请求即可。 我们可以直接在浏览器里面输入这个 URL，也可以使用 requests 等请求库来完成：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">import requests</span><br><span class="line"></span><br><span class="line">response = requests.<span class="builtin-name">get</span>(url)</span><br><span class="line"><span class="builtin-name">print</span>(response.json())</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>接口会返回如下格式的内容：</p>
                  <figure class="highlight 1c">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;'status': <span class="number">1</span>, 'request': '<span class="number">6291941969</span>5'&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里它返回了一个 JSON 格式的数据，其中 status 代表请求状态，如果是 1 的话，代表请求成功，另外其还会包含一个 request 字段，其内容是一个 ID，这个 ID 就是识别这个验证码的任务的 ID。因为 2Captcha 背后有很多人来帮助识别验证码，所以 2Captcha 将每个验证码的识别划分为一个个任务，每个任务都有一个唯一的 ID，刚分配任务时，这个任务被标记为 NOT_READY 状态。这些任务接下来会被分发给一个个人，识别完成之后，该任务就会被标记为已经识别状态，同时附有识别之后的信息，如 token 等内容。 好，刚才的接口请求成功之后，这个 reCAPTCHA 的识别任务就已经被下发了，其背后会有对应的人来对这个 reCAPTCHA 验证码进行识别，识别过程可能需要十几秒到几十秒不等，我们可以通过另一个接口来获取任务的结果。 获取结果的接口地址为：<a href="https://2captcha.com/res.php，同样我们需要传入一些参数，其参数介绍如下" target="_blank" rel="noopener">https://2captcha.com/res.php，同样我们需要传入一些参数，其参数介绍如下</a>：</p>
                  <p>参数</p>
                  <p><strong>类型</strong></p>
                  <p><strong>必需</strong></p>
                  <p><strong>描述</strong></p>
                  <p>key</p>
                  <p>String</p>
                  <p>Yes</p>
                  <p>API KEY</p>
                  <p>action</p>
                  <p>String</p>
                  <p>Yes</p>
                  <p>get，表示获取验证码的结果</p>
                  <p>id</p>
                  <p>Integer</p>
                  <p>Yes</p>
                  <p>任务 ID，就是刚才 in.php 接口返回的结果。</p>
                  <p>json</p>
                  <p>Integer Default: 0</p>
                  <p>No</p>
                  <p>返回 JSON 格式，1 代表使用 JSON 格式，0 代表纯文本格式</p>
                  <p>在这里我们构造一个 URL，它包括如上的参数：</p>
                  <ul>
                    <li>key：在这里换成你的 API KEY</li>
                    <li>action：就直接赋值 get</li>
                    <li>id：任务 ID</li>
                    <li>json：在这里用 1，即返回 JSON 格式数据</li>
                  </ul>
                  <p>这样我们就构造了如下的 URL：</p>
                  <figure class="highlight sas">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">https://2captcha.com/res.php?<span class="meta">key</span>=c0ae5935d807c28f285e5cb16c676a48<span class="variable">&amp;action</span>=get<span class="variable">&amp;id</span>=62919419695<span class="variable">&amp;json</span>=1</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>同样我们可以在浏览器中访问或者用 requests 请求，得到如下结果：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;</span><br><span class="line">    <span class="attr">"status"</span>: <span class="number">1</span>,</span><br><span class="line">    <span class="attr">"request"</span>: <span class="string">"03AOLTBLSg0fQUUMtP2o3kvJWNm6zla8MEjP_vPh629-xS-d_QrlJwMcxQfSJMUIU92noqbJ16yt5a0PdB3ORW-5MEbqK7NZ82bTnSZohCG_mYVVv8TbuvM1A99DFvlepxGEKlGCoi5lTHJd5z_QQ2mV1trGlI8VJkHjVAhLZzlz67MVgQzIu7aDl39n6aocAIVueQuCyjmA1C3hUECxpNlXJuXYVD10eJrqY_Bu36_2E0uBrmDIkAIjxCzEZWgadToU4ByLReOrNJ7_4t-P8leTUbPC5YBXvoDZZZByz8-vNnHzUu3GNNESzGSCMFfVPYumnXXI6i7TO5p1k-AElgb7qor6vDJGA_RpNNSUgAj8B0synG9APpbMQ4cEprHXle5pJtNCBX_v_8uqJLobomIx0St5l_H1tHGuTgI2UU-nWmR9TwvKp6SR-6G2Fi6pv7c8350tPbqJWWMcV0AXdfM85GjRDh2t7wh1CMukLQE21aIIwHh88kR0Fh0481Kw_umw8IfFCHyHKu8IcTERUL5LZdDzQkiGdF1wqWP-GhySMXEx-eOT7tB6SqPEAmO_mbwtJtA-qKzcHP"</span></span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>如果其返回的是如上格式的数据，就代表 reCAPTCHA 验证码已经识别成功了，其返回的 request 字段的内容就是识别的 token，我们直接拿着这个 token 放到表单里面提交就成功了。 那这个 token 怎么来用呢？ 其实如果不走 2Captcha 接口，我们如果人工验证成功之后，在其表单里面会把一个 name 叫做 g-recaptcha-response 的 textarea 赋值，如果验证成功，它的 value 值就是验证之后得到的 token，这个会作为表单提交的一部分发送到服务器进行验证。如果这个字段校验成功了，那就没问题了。 <img src="https://qiniu.cuiqingcai.com/2019-12-26-024847.png" alt=""> 所以，2Captcha 相当于为我们模拟了点选验证码的过程，其最终得到的这个 token 其实就是我们应该赋值给 name 为 g-recaptcha-response 的内容。 那么怎么赋值呢？ 很简单，用 JavaScript 就好了。我们可以用 JavaScript 选取到这个 textarea，然后直接赋值即可，代码如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">document.getElementById(<span class="string">"g-recaptcha-response"</span>).<span class="attribute">innerHTML</span>=<span class="string">"TOKEN_FROM_2CAPTCHA"</span>;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>注意这里的 <code>TOKEN_FROM_2CAPTCHA</code> 需要换成刚才我们所得到的 token 值。我们做爬虫模拟登录的时候，假如是用 Selenium、Puppeteer 等软件，在模拟程序里面，只需要模拟执行这段 JavaScript 代码，就可以成功赋值了。 执行之后，直接提交表单，我们查看下 Network 请求： <img src="https://qiniu.cuiqingcai.com/2019-12-26-025745.png" alt=""> 可以看到其就是提交了一个表单，其中有一个字段就是 g-recaptcha-response，它会发送到服务端进行校验，校验通过，那就成功了。 所以，如果我们借助于 2Captcha 得到了这个 token，然后把它赋值到表单的 textarea 里面，表单就会提交，如果 token 有效，就能成功绕过登录，而不需要我们再去点选验证码了。 最后我们得到如下成功的页面： <img src="https://qiniu.cuiqingcai.com/2019-12-26-030006.png" alt=""> 至此，我们就成功地借助 2Captcha 来完成了 reCAPTCHA V2 验证码的识别。</p>
                  <h2 id="总结"><a href="#总结" class="headerlink" title="总结"></a>总结</h2>
                  <p>本节我们介绍了利用 2Captcha 来帮助识别 reCAPTCHA V2 的流程。那应该来讲，我觉得工程师使用这样的服务并不是一种令人羞耻的过程，尤其是他可以以比较低的价格实现你的需求的情况下。毕竟你的时间，本身就是一种价值。 最后 2Captcha 这个网站我放在下面，有感兴趣的朋友可以看一下。另外如果有什么爬虫方面想看的文章，也欢迎在下面留言，我们会挑选被点赞较多的主题尽快写文。谢谢！</p>
                  <p><a href="http://2captcha.com/zh" target="_blank" rel="noopener">2captcha.com/zh</a></p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-27 08:55:16" itemprop="dateCreated datePublished" datetime="2019-12-27T08:55:16+08:00">2019-12-27</time>
                </span>
                <span id="/8637.html" class="post-meta-item leancloud_visitors" data-flag-title="谷歌验证码 ReCaptcha 破解教程，简单方便从零开始。" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>6.3k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>6 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8627.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8627.html" class="post-title-link" itemprop="url">严选高质量文章 - 爬虫工程师必看，深入解读字体反爬虫</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>内容选自<strong>即将出版</strong>的《Python3 反爬虫原理与绕过实战》，本次公开书稿范围为第 6 章——文本混淆反爬虫。本篇为第 6 章中的第 4 小节，其余小节将<strong>逐步放送</strong>。 <img src="http://can.sfhfpc.com/sfhfpc/20191226081009.jpg" alt=""></p>
                  <h2 id="字体反爬虫开篇概述"><a href="#字体反爬虫开篇概述" class="headerlink" title="字体反爬虫开篇概述"></a>字体反爬虫开篇概述</h2>
                  <p>在 CSS3 之前，Web 开发者必须使用用户计算机上已有的字体。但是在 CSS3 时代，开发者可以使用@font-face 为网页指定字体，对用户计算机字体的依赖。开发者可将心仪的字体文件放在 Web 服务器上，并在 CSS 样式中使用它。用户使用浏览器访问 Web 应用时，对应的字体会被浏览器下载到用户的计算机上。 在学习浏览器和页面渲染的相关知识时，我们了解到 CSS 的作用是修饰 HTML ，所以在页面渲染的时候不会改变 HTML 文档内容。由于字体的加载和映射工作是由 CSS 完成的，所以即使我们借助 Splash、Selenium 和 Puppeteer 工具也无法获得对应的文字内容。字体反爬虫正是利用了这个特点，将自定义字体应用到网页中重要的数据上，使得爬虫程序无法获得正确的数据。</p>
                  <h3 id="6-4-1-字体反爬虫示例"><a href="#6-4-1-字体反爬虫示例" class="headerlink" title="6.4.1 字体反爬虫示例"></a>6.4.1 字体反爬虫示例</h3>
                  <p>示例 7：字体反爬虫示例。 网址：<a href="http://www.porters.vip/confusion/movie.html" target="_blank" rel="noopener">http://www.porters.vip/confusion/movie.html</a>。 任务：爬取影片信息展示页中的影片评分、评价人数和票房数据，页面内容如图 6-32 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191226063524.jpg" alt=""> 图 6-32 示例 7 页面 在编写代码之前，我们需要确定目标数据的元素定位。定位时，我们在 HTML 中发现了一些奇怪的符号，HTML 代码如下：</p>
                  <figure class="highlight applescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"movie-index"</span>&gt; </span><br><span class="line">   &lt;p <span class="built_in">class</span>=<span class="string">"movie-index-title"</span>&gt;用户评分&lt;/p&gt; </span><br><span class="line">   &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"movie-index-content score normal-score"</span>&gt; </span><br><span class="line">       &lt;span <span class="built_in">class</span>=<span class="string">"index-left info-num "</span>&gt; </span><br><span class="line">       &lt;span <span class="built_in">class</span>=<span class="string">"stonefont"</span>&gt; ☒.☒ &lt;/span&gt; </span><br><span class="line">       &lt;/span&gt; </span><br><span class="line">   &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"index-right"</span>&gt; </span><br><span class="line">   &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"star-wrapper"</span>&gt; </span><br><span class="line">   &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"star-on"</span> style=<span class="string">"width:90%;"</span>&gt;&lt;/<span class="keyword">div</span>&gt; </span><br><span class="line">   &lt;/<span class="keyword">div</span>&gt; </span><br><span class="line">        &lt;span <span class="built_in">class</span>=<span class="string">"score-num"</span>&gt;&lt;span <span class="built_in">class</span>=<span class="string">"stonefont"</span>&gt; ☒☒. ☒☒ 万&lt;/span&gt;人评分&lt;/span&gt; </span><br><span class="line">   &lt;/<span class="keyword">div</span>&gt; </span><br><span class="line">   &lt;/<span class="keyword">div</span>&gt; </span><br><span class="line">&lt;/<span class="keyword">div</span>&gt;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>页面中重要的数据都是一些奇怪的字符，本应该显示“9.7”的地方在 HTML 中显示的是“☒.☒”，而本应该显示“56.83”的地方在 HTML 中显示的是“☒☒.☒☒”。与 6.3 节中的映射反爬虫不同，案例中的文字都被“☒”符号代替了，根本无法分辨。这就很奇怪了，“☒”能代表这么多种数字吗？ 要注意的是，Chrome 开发者工具的元素面板中显示的内容不一定是相应正文的原文，要想知道“☒”符号是什么，还需要到网页源代码中确认。对应的网页源代码如下：</p>
                  <figure class="highlight applescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"movie-index"</span>&gt;</span><br><span class="line">    &lt;p <span class="built_in">class</span>=<span class="string">"movie-index-title"</span>&gt;用户评分&lt;/p&gt;</span><br><span class="line">    &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"movie-index-content score normal-score"</span>&gt;</span><br><span class="line">        &lt;span <span class="built_in">class</span>=<span class="string">"index-left info-num "</span>&gt;</span><br><span class="line">            &lt;span <span class="built_in">class</span>=<span class="string">"stonefont"</span>&gt;&amp;<span class="comment">#xe624.&amp;#xe9c7&lt;/span&gt;</span></span><br><span class="line">        &lt;/span&gt;</span><br><span class="line">        &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"index-right"</span>&gt;</span><br><span class="line">          &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"star-wrapper"</span>&gt;</span><br><span class="line">            &lt;<span class="keyword">div</span> <span class="built_in">class</span>=<span class="string">"star-on"</span> style=<span class="string">"width:90%;"</span>&gt;&lt;/<span class="keyword">div</span>&gt;</span><br><span class="line">          &lt;/<span class="keyword">div</span>&gt;</span><br><span class="line">          &lt;span <span class="built_in">class</span>=<span class="string">"score-num"</span>&gt;&lt;span <span class="built_in">class</span>=<span class="string">"stonefont"</span>&gt;&amp;<span class="comment">#xf593&amp;#xe9c7&amp;#xe9c7.&amp;#xe624万&lt;/span&gt;人评分&lt;/span&gt;</span></span><br><span class="line">        &lt;/<span class="keyword">div</span>&gt;</span><br><span class="line">    &lt;/<span class="keyword">div</span>&gt;</span><br><span class="line">&lt;/<span class="keyword">div</span>&gt;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>从网页源代码中看到的并不是符号，而是由&amp;#x 开头的一些字符，这与示例 6 中的 SVG 映射反爬虫非常相似。我们将页面显示的数字与网页源代码中的字符进行比较，映射关系如图 6-33 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191226064028.jpg" alt=""> 图 6-33 字符与数字的映射关系 字符与数字是一一对应的，我们只需要多找一些页面，将 0 ~ 9 数字对应的字符凑齐即可。但如果目标网站的字体是动态变化的呢？映射关系也是变化的呢？ 根据 6.3 节的学习和分析，我们知道人为映射并不能解决这些问题，必须找到映射关系的规律，并使用 Python 代码实现映射算法才行。继续往下分析，难道字符映射是先异步加载数据再使用 JavaScript 渲染的？ <img src="http://can.sfhfpc.com/sfhfpc/20191226064115.jpg" alt=""> 图 6-34 请求记录 网络请求记录如图 6-34 所示，请求记录中并没有发现异步请求，这个猜测并没有得到证实。CSS 样式方面有没有线索呢？页面中包裹符号的标签的 class 属性值都是 stonefont：</p>
                  <figure class="highlight clean">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&lt;span <span class="keyword">class</span>=<span class="string">"stonefont"</span>&gt;&amp;#xe624.&amp;#xe9c7&lt;/span&gt; </span><br><span class="line">&lt;span <span class="keyword">class</span>=<span class="string">"stonefont"</span>&gt;&amp;#xf593&amp;#xe9c7&amp;#xe9c7.&amp;#xe624 万&lt;/span&gt; </span><br><span class="line">&lt;span <span class="keyword">class</span>=<span class="string">"stonefont"</span>&gt;&amp;#xea16&amp;#xe339.&amp;#xefd4&amp;#xf19a&lt;/span&gt;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>但对应的 CSS 样式中仅设置了字体：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-class">.stonefont</span> &#123; </span><br><span class="line">    <span class="attribute">font-family</span>: stonefont; </span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>既然是自定义字体，就意味着会加载字体文件，我们可以在网络请求中找到加载的字体文件 movie.woff，并将其下载到本地，接着使用百度字体编辑器看一看里面的内容。 百度字体编辑器 FontEditor （详见 <a href="http://fontstore.baidu.com/static/editor/index.html）是一款在线字体编辑软件，能够打开本地或者远程的" target="_blank" rel="noopener">http://fontstore.baidu.com/static/editor/index.html）是一款在线字体编辑软件，能够打开本地或者远程的</a> ttf、woff、eot、otf 格式的字体文件，具备这些格式字体文件的导入和导出功能，并且提供字形编辑、轮廓编辑和字体实时预览功能，界面如图 6-35 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191226064238.jpg" alt=""> 图 6-35 百度字体编辑器界面 打开页面后，将 movie.woff 文件拖曳到百度字体编辑器的灰色区域即可，字体文件内容如图 6-36 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191226064335.jpg" alt=""> 图 6-36 字体文件 movie.woff 预览 该字体文件中共有 12 个字体块，其中包括 2 个空白字体块和 0 ~ 9 的数字字体块。我们可以大胆地猜测，评分数据和票房数据中使用的数字正是从此而来。 由此看来，我们还需要了解一些字体文件格式相关的知识，在了解文件格式和规律后，才能够找到更合理的解决办法。</p>
                  <h3 id="6-4-2-字体文件-WOFF"><a href="#6-4-2-字体文件-WOFF" class="headerlink" title="6.4.2 字体文件 WOFF"></a>6.4.2 字体文件 WOFF</h3>
                  <p>WOFF（Web Open Font Format，Web 开放字体格式）是一种网页所采用的字体格式标准。本质上基于 SFNT 字体（如 TrueType），所以它具备 TrueType 的字体结构，我们只需要了解 TrueType 字体的相关知识即可。 TrueType 字体是苹果公司与微软公司联合开发的一种计算机轮廓字体，TrueType 字体中的每个字形由网格上的一系列点描述，点是字体中的最小单位，字形与点的关系如图 6-37 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191226064442.jpg" alt=""> 图 6-37 字形与点的关系 字体文件中不仅包含字形数据和点信息，还包括字符到字形映射、字体标题、命名和水平指标等，这些信息存在对应的表中，所以我们也可以认为 TrueType 字体文件由一系列的表组成，其中常用的表 及其作用如图 6-38 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191226064528.jpg" alt=""> 图 6-38 构成字体文件的常用表及其作用 如何查看这些表的结构和所包含的信息呢？我们可以借助第三方 Python 库 fonttools 将 WOFF 等字体文件转换成 XML 文件，这样就能查看字体文件的结构和表信息了。首先我们要安装 fonttools 库， 安装命令为：</p>
                  <figure class="highlight cmake">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">$ pip <span class="keyword">install</span> fonttools</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>安装完毕后就可以利用该库转换文件类型，对应的 Python 代码为：</p>
                  <figure class="highlight clean">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> fontTools.ttLib <span class="keyword">import</span> TTFont </span><br><span class="line">font = TTFont(<span class="string">'movie.woff'</span>) # 打开当前目录的 movie.woff 文件</span><br><span class="line">font.saveXML(<span class="string">'movie.xml'</span>) # 另存为 movie.xml</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>代码运行后就会在当前目录生成名为 movie 的 XML 文件。文件中字符到字形映射表 cmap 的内容如下：</p>
                  <figure class="highlight armasm">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&lt;cmap_format_4 platformID=<span class="string">"0"</span> platEncID=<span class="string">"3"</span> language=<span class="string">"0"</span>&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0x78"</span> name=<span class="string">"x"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xe339"</span> name=<span class="string">"uniE339"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xe624"</span> name=<span class="string">"uniE624"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xe7df"</span> name=<span class="string">"uniE7DF"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xe9c7"</span> name=<span class="string">"uniE9C7"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xea16"</span> name=<span class="string">"uniEA16"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xee76"</span> name=<span class="string">"uniEE76"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xefd4"</span> name=<span class="string">"uniEFD4"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xf19a"</span> name=<span class="string">"uniF19A"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xf57b"</span> name=<span class="string">"uniF57B"</span>/&gt; </span><br><span class="line">   &lt;<span class="meta">map</span> <span class="meta">code</span>=<span class="string">"0xf593"</span> name=<span class="string">"uniF593"</span>/&gt; </span><br><span class="line">&lt;/cmap_format_4&gt;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>map 标签中的 code 代表字符，name 代表字形名称，关系如图 6-39 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191226064718.jpg" alt=""> 图 6-39 字符到字形映射关系示例 XML 中的字符 0xe339 与网页源代码中的字符 对应，这样我们就确定了 HTML 中的字符码与 movie.woff 字体文件中对应的字形关系。字形数据存储在 glyf 表中，每个字形的数据都是独立的，例如字形 uniE339 的字形数据如下：</p>
                  <figure class="highlight vim">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&lt;TTGlyph name=<span class="string">"uniE339"</span> xMin=<span class="string">"0"</span> yMin=<span class="string">"-12"</span> xMax=<span class="string">"510"</span> yMax=<span class="string">"719"</span>&gt; </span><br><span class="line">   <span class="symbol">&lt;contour&gt;</span> </span><br><span class="line">     &lt;<span class="keyword">pt</span> <span class="keyword">x</span>=<span class="string">"410"</span> <span class="keyword">y</span>=<span class="string">"534"</span> <span class="keyword">on</span>=<span class="string">"1"</span>/&gt; </span><br><span class="line">     &lt;<span class="keyword">pt</span> <span class="keyword">x</span>=<span class="string">"398"</span> <span class="keyword">y</span>=<span class="string">"586"</span> <span class="keyword">on</span>=<span class="string">"0"</span>/&gt; </span><br><span class="line">     &lt;<span class="keyword">pt</span> <span class="keyword">x</span>=<span class="string">"377"</span> <span class="keyword">y</span>=<span class="string">"609"</span> <span class="keyword">on</span>=<span class="string">"1"</span>/&gt; </span><br><span class="line">     &lt;<span class="keyword">pt</span> <span class="keyword">x</span>=<span class="string">"341"</span> <span class="keyword">y</span>=<span class="string">"646"</span> <span class="keyword">on</span>=<span class="string">"0"</span>/&gt; </span><br><span class="line">     &lt;<span class="keyword">pt</span> <span class="keyword">x</span>=<span class="string">"289"</span> <span class="keyword">y</span>=<span class="string">"646"</span> <span class="keyword">on</span>=<span class="string">"1"</span>/&gt; </span><br><span class="line">     ... </span><br><span class="line">   &lt;/contour&gt; </span><br><span class="line">   <span class="symbol">&lt;contour&gt;</span> </span><br><span class="line">     &lt;<span class="keyword">pt</span> <span class="keyword">x</span>=<span class="string">"139"</span> <span class="keyword">y</span>=<span class="string">"232"</span> <span class="keyword">on</span>=<span class="string">"1"</span>/&gt; </span><br><span class="line">     &lt;<span class="keyword">pt</span> <span class="keyword">x</span>=<span class="string">"139"</span> <span class="keyword">y</span>=<span class="string">"188"</span> <span class="keyword">on</span>=<span class="string">"0"</span>/&gt; </span><br><span class="line">     &lt;<span class="keyword">pt</span> <span class="keyword">x</span>=<span class="string">"178"</span> <span class="keyword">y</span>=<span class="string">"103"</span> <span class="keyword">on</span>=<span class="string">"0"</span>/&gt; </span><br><span class="line">     ... </span><br><span class="line">   &lt;/contour&gt; </span><br><span class="line">   &lt;instructions/&gt; </span><br><span class="line">&lt;/TTGlyph&gt;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>TTGlyph 标签中记录着字形的名称、<em>x</em> 轴坐标和 <em>y</em> 轴坐标（坐标也可以理解为字形的宽高）。contour 标签记录的是字形的轮廓信息，也就是多个点的坐标位置，正是这些点构成了如图 6-40 所示的字形。 <img src="http://can.sfhfpc.com/sfhfpc/20191226064821.jpg" alt=""> 图 6-40 字形 uniE339 的轮廓 我们可以在百度字体编辑器中调整点的位置，然后保存字体文件并将新字体文件转换为 XML 格式，相同名称的字形数据如下：</p>
                  <figure class="highlight applescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&lt;TTGlyph <span class="built_in">name</span>=<span class="string">"uniE339"</span> xMin=<span class="string">"115"</span> yMin=<span class="string">"6"</span> xMax=<span class="string">"430"</span> yMax=<span class="string">"495"</span>&gt; </span><br><span class="line"> &lt;contour&gt; </span><br><span class="line">   &lt;pt x=<span class="string">"400"</span> y=<span class="string">"352"</span> <span class="keyword">on</span>=<span class="string">"1"</span>/&gt; </span><br><span class="line">   &lt;pt x=<span class="string">"356"</span> y=<span class="string">"406"</span> <span class="keyword">on</span>=<span class="string">"0"</span>/&gt; </span><br><span class="line">   &lt;pt x=<span class="string">"342"</span> y=<span class="string">"421"</span> <span class="keyword">on</span>=<span class="string">"1"</span>/&gt; </span><br><span class="line">   &lt;pt x=<span class="string">"318"</span> y=<span class="string">"446"</span> <span class="keyword">on</span>=<span class="string">"0"</span>/&gt; </span><br><span class="line">   &lt;pt x=<span class="string">"283"</span> y=<span class="string">"446"</span> <span class="keyword">on</span>=<span class="string">"1"</span>/&gt; </span><br><span class="line">   ... </span><br><span class="line"> &lt;/contour&gt; </span><br><span class="line"> &lt;instructions/&gt; </span><br><span class="line">&lt;/TTGlyph&gt;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>接着将调整前的字形数据和调整后的字形数据进行对比。 如图 6-41 所示，点的位置调整后，字形数据也会发生相应的变化，如 xMin、xMax、yMin、yMax 还有 pt 标签中的 x 坐标 y 坐标都与之前的不同了。 <img src="http://can.sfhfpc.com/sfhfpc/20191226064932.jpg" alt=""> 图 6-41 字形数据对比 XML 文件中记录的是字形坐标信息，实际上，我们没有办法直接通过字形数据获得文字，只能从其他方面想办法。虽然目标网站使用多套字体，但相同文字的字形也是相同的。比如现在有 movie.woff 和 food.woff 这两套字体，它们包含的字形如下：</p>
                  <figure class="highlight applescript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># movie.woff </span></span><br><span class="line"><span class="comment"># 包含 10 个字形数据：[0123456789] </span></span><br><span class="line">&lt;cmap_format_4 platformID=<span class="string">"0"</span> platEncID=<span class="string">"3"</span> language=<span class="string">"0"</span>&gt; </span><br><span class="line">   &lt;map code=<span class="string">"0x78"</span> <span class="built_in">name</span>=<span class="string">"x"</span>/&gt; </span><br><span class="line">   &lt;map code=<span class="string">"0xe339"</span> <span class="built_in">name</span>=<span class="string">"uniE339"</span>/&gt; <span class="comment"># 数字 6 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xe624"</span> <span class="built_in">name</span>=<span class="string">"uniE624"</span>/&gt; <span class="comment"># 数字 9 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xe7df"</span> <span class="built_in">name</span>=<span class="string">"uniE7DF"</span>/&gt; <span class="comment"># 数字 2 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xe9c7"</span> <span class="built_in">name</span>=<span class="string">"uniE9C7"</span>/&gt; <span class="comment"># 数字 7 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xea16"</span> <span class="built_in">name</span>=<span class="string">"uniEA16"</span>/&gt; <span class="comment"># 数字 5 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xee76"</span> <span class="built_in">name</span>=<span class="string">"uniEE76"</span>/&gt; <span class="comment"># 数字 0 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xefd4"</span> <span class="built_in">name</span>=<span class="string">"uniEFD4"</span>/&gt; <span class="comment"># 数字 8 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xf19a"</span> <span class="built_in">name</span>=<span class="string">"uniF19A"</span>/&gt; <span class="comment"># 数字 3 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xf57b"</span> <span class="built_in">name</span>=<span class="string">"uniF57B"</span>/&gt; <span class="comment"># 数字 1</span></span><br><span class="line">   &lt;map code=<span class="string">"0xf593"</span> <span class="built_in">name</span>=<span class="string">"uniF593"</span>/&gt; <span class="comment"># 数字 4 </span></span><br><span class="line">&lt;/cmap_format_4&gt; </span><br><span class="line"></span><br><span class="line"><span class="comment"># food.woff </span></span><br><span class="line"><span class="comment"># 包含 3 个字形数据：[012] </span></span><br><span class="line">&lt;cmap_format_4 platformID=<span class="string">"0"</span> platEncID=<span class="string">"3"</span> language=<span class="string">"0"</span>&gt; </span><br><span class="line">   &lt;map code=<span class="string">"0x78"</span> <span class="built_in">name</span>=<span class="string">"x"</span>/&gt; </span><br><span class="line">   &lt;map code=<span class="string">"0xe556"</span> <span class="built_in">name</span>=<span class="string">"uniE556"</span>/&gt; <span class="comment"># 数字 0 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xe667"</span> <span class="built_in">name</span>=<span class="string">"uniE667"</span>/&gt; <span class="comment"># 数字 1 </span></span><br><span class="line">   &lt;map code=<span class="string">"0xe778"</span> <span class="built_in">name</span>=<span class="string">"uniE778"</span>/&gt; <span class="comment"># 数字 2 </span></span><br><span class="line">&lt;/cmap_format_4&gt;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>要实现自动识别文字，需要先准备参照字形，也就是人为地准备数字 0 ~ 9 的字形映射关系和字形数据，如：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># 0 和 7 与字形名称的映射伪代码，data 键对应的值是字形数据</span></span><br><span class="line"><span class="attr">font_mapping</span> = [ </span><br><span class="line">   &#123;<span class="string">'name'</span>: <span class="string">'uniE9C7'</span>, <span class="string">'words'</span>: <span class="string">'7'</span>, <span class="string">'data'</span>: <span class="string">'uniE9C7_contour_pt'</span>&#125;, </span><br><span class="line">   &#123;<span class="string">'name'</span>: <span class="string">'uniEE76'</span>, <span class="string">'words'</span>: <span class="string">'0'</span>, <span class="string">'data'</span>: <span class="string">'uniEE76_countr_pt'</span>&#125;, </span><br><span class="line">]</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>当我们遇到目标网站上其他字体文件时，就可以使用参照字形中的字形数据与目标字形进行匹配，如果字形数据非常接近，就认为这两个字形描述的是相同的文字。字形数据包含记录字形名称和字形起止坐标的 TTGlyph 标签以及记录点坐标的 pt 标签，起止坐标代表的是字形在画布上的位置，点坐标代表字形中每个点在画布上的位置。在起止坐标中，<em>x</em> 轴差值代表字形宽度，<em>y</em> 轴差值代表字形高度。 如图 6-42 所示，两个字形的起止坐标和宽高都有很大的差别，但是却能够描述相同的文字，所以字形在画布中的位置并不会影响描述的文字，字形宽度和字形高度也不会影响描述的文字。 <img src="http://can.sfhfpc.com/sfhfpc/20191226065110.jpg" alt=""> 图 6-42 描述相同文字的两个字形 点坐标的数量和坐标值可以作为比较条件吗？ 如图 6-43 所示，两个不同文字的字形数据是不一样的。虽然这两种字形的 name 都是 uniE9C7，但是字形数据中大部分 pt 标签 x 和 y 的差距都很大，所以我们可以判定这两个字形描述的并不是 同一个文字。你可能会想到点的数量也可以作为排除条件，也就是说如果点的数量不相同，那么这个 两个字形描述的就不是同一个文字。真的是这样吗？ <img src="http://can.sfhfpc.com/sfhfpc/20191226065656.jpg" alt=""> 图 6-43 描述不同文字的字形数据对比 在图 6-44 中，左侧描述文字 7 的字形有 17 个点，而右侧描述文字 7 的字形却有 20 个点。对应的字形信息如图 6-45 所示。 <img src="http://can.sfhfpc.com/sfhfpc/20191226065745.jpg" alt=""> 图 6-44 描述相同文字的字形 <img src="http://can.sfhfpc.com/sfhfpc/20191226065822.jpg" alt=""> 图 6-45 描述相同文字的字形信息 虽然点的数量不一样，但是它们的字形并没有太大的变化，也不会造成用户误读，所以点的数量并不能作为排除不同字形的条件。因此，只有起止坐标和点坐标数据完全相同的字形，描述的才是相同字符。</p>
                  <h3 id="6-4-3-字体反爬虫绕过实战"><a href="#6-4-3-字体反爬虫绕过实战" class="headerlink" title="6.4.3 字体反爬虫绕过实战"></a>6.4.3 字体反爬虫绕过实战</h3>
                  <p>要确定两组字形数据描述的是否为相同字符，我们必须取出 HTML 中对应的字形数据，然后将待确认的字形与我们准备好的基准字形数据进行对比。现在我们来整理一下这一系列工作的步骤。 (1) 准备基准字形描述信息。 (2) 访问目标网页。 (3) 从目标网页中读取字体编码字符。 (4) 下载 WOFF 文件并用 Python 代码打开。 (5) 根据字体编码字符找到 WOFF 文件中的字形轮廓信息。 (6) 将该字形轮廓信息与基准字形轮廓信息进行对比。 (7) 得出对比结果。 我们先完成前 4 个步骤的代码。下载 WOFF 文件并将其中字形描述的文字与人类认知的文字进行映射。由于字形数据比较庞大，所以我们可以将字形数据进行散列计算，这样得到的结果既简短又唯一，不会影响对比结果。这里以数字 0 ~ 9 为例：</p>
                  <figure class="highlight armasm">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">base_font </span>= &#123; </span><br><span class="line"> <span class="string">"font"</span>: [&#123;<span class="string">"name"</span>: <span class="string">"uniEE76"</span>, <span class="string">"value"</span>: <span class="string">"0"</span>, <span class="string">"hex"</span>: <span class="string">"fc170db1563e66547e9100cf7784951f"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniF57B"</span>, <span class="string">"value"</span>: <span class="string">"1"</span>, <span class="string">"hex"</span>: <span class="string">"251357942c5160a003eec31c68a06f64"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniE7DF"</span>, <span class="string">"value"</span>: <span class="string">"2"</span>, <span class="string">"hex"</span>: <span class="string">"8a3ab2e9ca7db2b13ce198521010bde4"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniF19A"</span>, <span class="string">"value"</span>: <span class="string">"3"</span>, <span class="string">"hex"</span>: <span class="string">"712e4b5abd0ba2b09aff19be89e75146"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniF593"</span>, <span class="string">"value"</span>: <span class="string">"4"</span>, <span class="string">"hex"</span>: <span class="string">"e5764c45cf9de7f0a4ada6b0370b81a1"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniEA16"</span>, <span class="string">"value"</span>: <span class="string">"5"</span>, <span class="string">"hex"</span>: <span class="string">"c631abb5e408146eb1a17db4113f878f"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniE339"</span>, <span class="string">"value"</span>: <span class="string">"6"</span>, <span class="string">"hex"</span>: <span class="string">"0833d3b4f61f02258217421b4e4bde24"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniE9C7"</span>, <span class="string">"value"</span>: <span class="string">"7"</span>, <span class="string">"hex"</span>: <span class="string">"4aa5ac9a6741107dca4c5dd05176ec4c"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniEFD4"</span>, <span class="string">"value"</span>: <span class="string">"8"</span>, <span class="string">"hex"</span>: <span class="string">"c37e95c05e0dd147b47f3cb1e5ac60d7"</span>&#125;, </span><br><span class="line"> &#123;<span class="string">"name"</span>: <span class="string">"uniE624"</span>, <span class="string">"value"</span>: <span class="string">"9"</span>, <span class="string">"hex"</span>: <span class="string">"704362b6e0feb6cd0b1303f10c000f95"</span>&#125;] </span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>字典中的 name 代表该字形的名称，value 代表该字形描述的文字，hex 代表字形信息的 MD5 值。 考虑到网络请求记录中的字体文件路径有可能会变化，我们必须找到 CSS 中设定的字体文件路径，引入 CSS 的 HTML 代码为：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&lt;link <span class="attribute">href</span>=<span class="string">"./css/movie.css"</span> <span class="attribute">rel</span>=<span class="string">"stylesheet"</span>&gt;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>由引入代码得知该 CSS 文件的路径为 <a href="http://www.porters.vip/confusion/css/movie.css，文件中" target="_blank" rel="noopener">http://www.porters.vip/confusion/css/movie.css，文件中</a> @font-face 处就是设置字体的代码：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">@font-face</span> &#123; </span><br><span class="line">   <span class="attribute">font-family</span>: stonefont; </span><br><span class="line">   <span class="attribute">src</span>:<span class="built_in">url</span>(<span class="string">'../font/movie.woff'</span>) <span class="built_in">format</span>(<span class="string">'woff'</span>); </span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>字体文件路径为 <a href="http://www.porters.vip/confusion/font/movie.woff。找到文件后，我们就可以开始编写代码了，对应的" target="_blank" rel="noopener">http://www.porters.vip/confusion/font/movie.woff。找到文件后，我们就可以开始编写代码了，对应的</a> Python 代码如下：</p>
                  <figure class="highlight properties">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">import</span> <span class="string">re </span></span><br><span class="line"><span class="attr">from</span> <span class="string">parsel import Selector </span></span><br><span class="line"><span class="attr">from</span> <span class="string">urllib import parse </span></span><br><span class="line"><span class="attr">from</span> <span class="string">fontTools.ttLib import TTFont </span></span><br><span class="line"><span class="attr">url</span> = <span class="string">'http://www.porters.vip/confusion/movie.html' </span></span><br><span class="line"><span class="attr">resp</span> = <span class="string">requests.get(url) </span></span><br><span class="line"><span class="attr">sel</span> = <span class="string">Selector(resp.text) </span></span><br><span class="line"><span class="comment"># 提取页面加载的所有 css 文件路径</span></span><br><span class="line"><span class="attr">css_path</span> = <span class="string">sel.css('link[rel=stylesheet]::attr(href)').extract() </span></span><br><span class="line"><span class="attr">woffs</span> = <span class="string">[] </span></span><br><span class="line"><span class="attr">for</span> <span class="string">c in css_path: </span></span><br><span class="line"><span class="comment">   # 拼接正确的 css 文件路径</span></span><br><span class="line">   <span class="attr">css_url</span> = <span class="string">parse.urljoin(url, c) </span></span><br><span class="line"><span class="comment">   # 向 css 文件发起请求</span></span><br><span class="line">   <span class="attr">css_resp</span> = <span class="string">requests.get(css_url) </span></span><br><span class="line"><span class="comment">   # 匹配 css 文件中的 woff 文件路径</span></span><br><span class="line">   <span class="attr">woff_path</span> = <span class="string">re.findall("src:url('..(.*.woff)') format('woff');", </span></span><br><span class="line">   <span class="attr">css_resp.text)</span></span><br><span class="line">   <span class="attr">if</span> <span class="string">woff_path: </span></span><br><span class="line"><span class="comment">       # 如故路径存在则添加到 woffs 列表中</span></span><br><span class="line">       <span class="attr">woffs</span> <span class="string">+= woff_path </span></span><br><span class="line"><span class="attr">woff_url</span> = <span class="string">'http://www.porters.vip/confusion' + woffs.pop() </span></span><br><span class="line"><span class="attr">woff</span> = <span class="string">requests.get(woff_url) </span></span><br><span class="line"><span class="attr">filename</span> = <span class="string">'target.woff' </span></span><br><span class="line"><span class="attr">with</span> <span class="string">open(filename, 'wb') as f: </span></span><br><span class="line"><span class="comment">   # 将文件保存到本地</span></span><br><span class="line">   <span class="meta">f.write(woff.content)</span> <span class="string"></span></span><br><span class="line"><span class="comment"># 使用 TTFont 库打开刚才下载的 woff 文件</span></span><br><span class="line"><span class="attr">font</span> = <span class="string">TTFont(filename)</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>因为 TTFont 可以直接读取 woff 文件的结构，所以这里不需要将 woff 保存为 XML 文件。接着以评分数据 9.7 对应的编码 #xe624.#xe9c7 进行测试，在原来的代码中引入基准字体数据 base_font，然后新增以下代码：</p>
                  <figure class="highlight livecodeserver">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">web_code = <span class="string">'&amp;#xe624.&amp;#xe9c7'</span></span><br><span class="line"><span class="comment"># 编码文字替换</span></span><br><span class="line">woff_code = [i.<span class="built_in">upper</span>().<span class="built_in">replace</span>(<span class="string">'&amp;#X'</span>, <span class="string">'uni'</span>) <span class="keyword">for</span> i <span class="keyword">in</span> web_code.<span class="built_in">split</span>(<span class="string">'.'</span>)] </span><br><span class="line">import hashlib </span><br><span class="line"><span class="built_in">result</span> = [] </span><br><span class="line"><span class="keyword">for</span> w <span class="keyword">in</span> woff_code: </span><br><span class="line">   <span class="comment"># 从字体文件中取出对应编码的字形信息</span></span><br><span class="line">   content = font[<span class="string">'glyf'</span>].glyphs.<span class="built_in">get</span>(w).data </span><br><span class="line">   <span class="comment"># 字形信息 MD5 </span></span><br><span class="line">   glyph = hashlib.md5(content).hexdigest() </span><br><span class="line">   <span class="keyword">for</span> b <span class="keyword">in</span> base_font.<span class="built_in">get</span>(<span class="string">'font'</span>): </span><br><span class="line">       <span class="comment"># 与基准字形中的 MD5 值进行对比，如果相同则取出该字形描述的文字</span></span><br><span class="line">       <span class="keyword">if</span> b.<span class="built_in">get</span>(<span class="string">'hex'</span>) == glyph: </span><br><span class="line">           <span class="built_in">result</span>.append(b.<span class="built_in">get</span>(<span class="string">'value'</span>)) </span><br><span class="line">           break </span><br><span class="line"><span class="comment"># 打印映射结果</span></span><br><span class="line">print(<span class="built_in">result</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>以上代码运行结果为：</p>
                  <figure class="highlight scheme">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[<span class="symbol">'9</span>', <span class="symbol">'7</span>']</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果说明能够正确映射字体文件中字形描述的文字。</p>
                  <h3 id="6-4-4-小结"><a href="#6-4-4-小结" class="headerlink" title="6.4.4 小结"></a>6.4.4 小结</h3>
                  <p>字体反爬能给爬虫工程师带来很大的麻烦。虽然爬虫工程师找到了应对方法，但这种方法依赖的条件比较严苛，如果开发者频繁改动字体文件或准备多套字体文件并随机切换，那真是一件令爬虫工程师头疼的事。不过，这些工作对于开发者来说也不是轻松的事。</p>
                  <h2 id="新书福利"><a href="#新书福利" class="headerlink" title="新书福利"></a>新书福利</h2>
                  <p>真是翘首以盼！《Python3 反爬虫原理与绕过实战》一书终于要跟大家见面了！为了感谢大家对韦世东和本书的期待与支持，在新书发布时会举办多场送书活动和限时折扣活动。 <img src="http://can.sfhfpc.com/sfhfpc/20191226081009.jpg" alt=""> 想要与作者韦世东交流或者参加新书发布活动的朋友可以扫描二维码进群与我互动哦！</p>
                  <h3 id="转载说明"><a href="#转载说明" class="headerlink" title="转载说明"></a>转载说明</h3>
                  <p>本篇内容摘自出版图书《Python3 反爬虫原理与绕过实战》，欢迎各位好友与同行转载！ 记得带上相关的版权信息哦😊。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/韦世东学算法和反爬虫" class="author" itemprop="url" rel="index">韦世东学算法和反爬虫</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-26 08:46:01" itemprop="dateCreated datePublished" datetime="2019-12-26T08:46:01+08:00">2019-12-26</time>
                </span>
                <span id="/8627.html" class="post-meta-item leancloud_visitors" data-flag-title="严选高质量文章 - 爬虫工程师必看，深入解读字体反爬虫" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>11k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>10 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8602.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> 技术杂谈 <i class="label-arrow"></i>
                  </a>
                  <a href="/8602.html" class="post-title-link" itemprop="url">如何通过 Tampermonkey 快速查找 JavaScript 加密入口</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <p>在很多情况下，我们可能想要在网页中自动执行某些代码，帮助我们完成一些操作。如自动抢票、自动刷单、自动爬虫等等，这些操作绝大部分都是借助 JavaScript 来实现的。那么问题来了？在浏览器里面怎样才能方便地执行我们所期望执行的 JavaScript 代码呢？在这里推荐一个插件，叫做 Tampermonkey。这个插件的功能非常强大，利用它我们几乎可以在网页中执行任何 JavaScript 代码，实现我们想要的功能。 当然不仅仅是自动抢票、自动刷单、自动爬虫，Tampermonkey 的用途远远不止这些，只要我们想要的功能能用 JavaScript 实现，Tampermonkey 就可以帮我们做到。比如我们可以将 Tampermonkey 应用到 JavaScript 逆向分析中，去帮助我们更方便地分析一些 JavaScript 加密和混淆代码。 本节我们就来介绍一下这个插件的使用方法，并结合一个实际案例，介绍下这个插件在 JavaScript 逆向分析中的用途。</p>
                  <h2 id="Tampermonkey"><a href="#Tampermonkey" class="headerlink" title="Tampermonkey"></a>Tampermonkey</h2>
                  <p>Tampermonkey，中文也叫作「油猴」，它是一款浏览器插件，支持 Chrome。利用它我们可以在浏览器加载页面时自动执行某些 JavaScript 脚本。由于执行的是 JavaScript，所以我们几乎可以在网页中完成任何我们想实现的效果，如自动爬虫、自动修改页面、自动响应事件等等。</p>
                  <h2 id="安装"><a href="#安装" class="headerlink" title="安装"></a>安装</h2>
                  <p>首先我们需要安装 Tampermonkey，这里我们使用的浏览器是 Chrome。直接在 Chrome 应用商店或者在 Tampermonkey 的官网 <a href="https://www.tampermonkey.net/" target="_blank" rel="noopener">https://www.tampermonkey.net/</a> 下载安装即可。 安装完成之后，在 Chrome 浏览器的右上角会出现 Tampermonkey 的图标，这就代表安装成功了。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-123401.png" alt=""></p>
                  <h2 id="获取脚本"><a href="#获取脚本" class="headerlink" title="获取脚本"></a>获取脚本</h2>
                  <p>Tampermonkey 运行的是 JavaScript 脚本，每个网站都能有对应的脚本运行，不同的脚本能完成不同的功能。这些脚本我们可以自定义，同样也可以用已经写好的很多脚本，毕竟有些轮子有了，我们就不需要再去造了。 我们可以在 <a href="https://greasyfork.org/zh-CN/scripts" target="_blank" rel="noopener">https://greasyfork.org/zh-CN/scripts</a> 这个网站找到一些非常实用的脚本，如全网视频去广告、百度云全网搜索等等，大家可以体验一下。</p>
                  <h2 id="脚本编写"><a href="#脚本编写" class="headerlink" title="脚本编写"></a>脚本编写</h2>
                  <p>除了使用别人已经写好的脚本，我们也可以自己编写脚本来实现想要的功能。编写脚本难不难呢？其实就是写 JavaScript 代码，只要懂一些 JavaScript 的语法就好了。另外除了懂 JavaScript 语法，我们还需要遵循脚本的一些写作规范，这其中就包括一些参数的设置。 下面我们就简单实现一个小的脚本，实现某个功能。 首先我们可以点击 Tampermonkey 插件图标，点击「管理面板」按钮，打开脚本管理页面。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-130908.png" alt=""> 界面类似显示如下图所示。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-130941.png" alt=""> 在这里显示了我们已经有的一些 Tampermonkey 脚本，包括我们自行创建的，也包括从第三方网站下载安装的。 另外这里也提供了编辑、调试、删除等管理功能，我们可以方便地对脚本进行管理。 接下来我们来创建一个新的脚本来试试，点击左侧的「+」号，会显示如图所示的页面。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-131204.png" alt=""> 初始化的代码如下：</p>
                  <figure class="highlight php">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment">// ==UserScript==</span></span><br><span class="line"><span class="comment">// @name         New Userscript</span></span><br><span class="line"><span class="comment">// @namespace    http://tampermonkey.net/</span></span><br><span class="line"><span class="comment">// @version      0.1</span></span><br><span class="line"><span class="comment">// @description  try to take over the world!</span></span><br><span class="line"><span class="comment">// @author       You</span></span><br><span class="line"><span class="comment">// @match        https://www.tampermonkey.net/documentation.php?ext=dhdg</span></span><br><span class="line"><span class="comment">// @grant        none</span></span><br><span class="line"><span class="comment">// ==/UserScript==</span></span><br><span class="line"></span><br><span class="line">(<span class="function"><span class="keyword">function</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    <span class="string">'use strict'</span>;</span><br><span class="line"></span><br><span class="line">    <span class="comment">// Your code here...</span></span><br><span class="line">&#125;)();</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里最上面是一些注释，但这些注释是非常有用的，这部分内容叫做 <code>UserScript Header</code> ，我们可以在里面配置一些脚本的信息，如名称、版本、描述、生效站点等等。 下面简单介绍下 <code>UserScript Header</code> 的一些参数定义。</p>
                  <ul>
                    <li>@name：脚本的名称，就是在控制面板显示的脚本名称。</li>
                    <li>@namespace：脚本的命名空间。</li>
                    <li>@version：脚本的版本，主要是做版本更新时用。</li>
                    <li>@author：作者。</li>
                    <li>@description：脚本描述。</li>
                    <li>@homepage, @homepageURL, @website，@source：作者主页，用于在Tampermonkey选项页面上从脚本名称点击跳转。请注意，如果 @namespace 标记以 <code>http://</code>开头，此处也要一样。</li>
                    <li>@icon, @iconURL and @defaulticon：低分辨率图标。</li>
                    <li>@icon64 and @icon64URL：64x64 高分辨率图标。</li>
                    <li>@updateURL：检查更新的网址，需要定义 @version。</li>
                    <li>@downloadURL：更新下载脚本的网址，如果定义成 <code>none</code> 就不会检查更新。</li>
                    <li>@supportURL：报告问题的网址。</li>
                    <li>
                      <p>@include：生效页面，可以配置多个，但注意这里并不支持 URL Hash。 例如：</p>
                      <figure class="highlight jboss-cli">
                        <table>
                          <tr>
                            <td class="gutter">
                              <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                            </td>
                            <td class="code">
                              <pre><span class="line"><span class="string">//</span> @include http:<span class="string">//www.tampermonkey.net/</span>*</span><br><span class="line"><span class="string">//</span> @include http:<span class="string">//</span>*</span><br><span class="line"><span class="string">//</span> @include https:<span class="string">//</span>*</span><br><span class="line"><span class="string">//</span> @include *</span><br></pre>
                            </td>
                          </tr>
                        </table>
                      </figure>
                    </li>
                    <li>
                      <p>@match：约等于 @include 标签，可以配置多个。</p>
                    </li>
                    <li>@exclude：不生效页面，可配置多个，优先级高于 @include 和 @match。</li>
                    <li>
                      <p>@require：附加脚本网址，相当于引入外部的脚本，这些脚本会在自定义脚本执行之前执行，比如引入一些必须的库，如 jQuery 等，这里可以支持配置多个 @require 参数。 例如：</p>
                      <figure class="highlight vim">
                        <table>
                          <tr>
                            <td class="gutter">
                              <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                            </td>
                            <td class="code">
                              <pre><span class="line">// @require http<span class="variable">s:</span>//code.jquery.<span class="keyword">com</span>/jquery-<span class="number">2.1</span>.<span class="number">4</span>.<span class="built_in">min</span>.js</span><br><span class="line">// @require http<span class="variable">s:</span>//code.jquery.<span class="keyword">com</span>/jquery-<span class="number">2.1</span>.<span class="number">3</span>.<span class="built_in">min</span>.js#sha256=<span class="number">23456</span>...</span><br><span class="line">// @require http<span class="variable">s:</span>//code.jquery.<span class="keyword">com</span>/jquery-<span class="number">2.1</span>.<span class="number">2</span>.<span class="built_in">min</span>.js#md5=<span class="number">34567</span>...,<span class="built_in">sha256</span>=<span class="number">6789</span>...</span><br></pre>
                            </td>
                          </tr>
                        </table>
                      </figure>
                    </li>
                    <li>
                      <p>@resource：预加载资源，可通过 GM_getResourceURL 和 GM_getResourceText 读取。</p>
                    </li>
                    <li>@connect：允许被 GM_xmlhttpRequest 访问的域名，每行一个。</li>
                    <li>@run-at：脚本注入的时刻，如页面刚加载时，某个事件发生后等等。 例如：<ul>
                        <li>document-start：尽可能地早执行此脚本。</li>
                        <li>document-body：DOM 的 body 出现时执行。</li>
                        <li>document-end：DOMContentLoaded 事件发生时或发生后后执行。</li>
                        <li>document-idle：DOMContentLoaded 事件发生后执行，即 DOM 加载完成之后执行，这是默认的选项。</li>
                        <li>context-menu：如果在浏览器上下文菜单（仅限桌面 Chrome 浏览器）中点击该脚本，则会注入该脚本。注意：如果使用此值，则将忽略所有 @include 和 @exclude 语句。</li>
                      </ul>
                    </li>
                    <li>
                      <p>@grant：用于添加 GM 函数到白名单，相当于授权某些 GM 函数的使用权限。 例如：</p>
                      <figure class="highlight pgsql">
                        <table>
                          <tr>
                            <td class="gutter">
                              <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                            </td>
                            <td class="code">
                              <pre><span class="line">// @<span class="keyword">grant</span> GM_setValue</span><br><span class="line">// @<span class="keyword">grant</span> GM_getValue</span><br><span class="line">// @<span class="keyword">grant</span> GM_setClipboard</span><br><span class="line">// @<span class="keyword">grant</span> unsafeWindow</span><br><span class="line">// @<span class="keyword">grant</span> <span class="keyword">window</span>.<span class="keyword">close</span></span><br><span class="line">// @<span class="keyword">grant</span> <span class="keyword">window</span>.focus</span><br></pre>
                            </td>
                          </tr>
                        </table>
                      </figure>
                      <p>如果没有定义过 @grant 选项，Tampermonkey 会猜测所需要的函数使用情况。</p>
                    </li>
                    <li>@noframes：此标记使脚本在主页面上运行，但不会在 iframe 上运行。</li>
                    <li>
                      <p>@nocompat：由于部分代码可能是专门为专门的浏览器所写，通过此标记，Tampermonkey 会知道脚本可以运行的浏览器。 例如：</p>
                      <figure class="highlight 1c">
                        <table>
                          <tr>
                            <td class="gutter">
                              <pre><span class="line">1</span><br></pre>
                            </td>
                            <td class="code">
                              <pre><span class="line"><span class="comment">// @nocompat Chrome</span></span><br></pre>
                            </td>
                          </tr>
                        </table>
                      </figure>
                      <p>这样就指定了脚本只在 Chrome 浏览器中运行。</p>
                    </li>
                  </ul>
                  <p>除此之外，Tampermonkey 还定义了一些 API，使得我们可以方便地完成某个操作，如：</p>
                  <ul>
                    <li>GM_log：将日志输出到控制台。</li>
                    <li>GM_setValue：将参数内容保存到 Storage 中。</li>
                    <li>GM_addValueChangeListener：为某个变量添加监听，当这个变量的值改变时，就会触发回调。</li>
                    <li>GM_xmlhttpRequest：发起 Ajax 请求。</li>
                    <li>GM_download：下载某个文件到磁盘。</li>
                    <li>GM_setClipboard：将某个内容保存到粘贴板。</li>
                  </ul>
                  <p>还有很多其他的 API，大家可以到 <a href="https://www.tampermonkey.net/documentation.php" target="_blank" rel="noopener">https://www.tampermonkey.net/documentation.php</a> 来查看更多的内容。 在 <code>UserScript Header</code> 下方是 JavaScript 函数和调用的代码，其中 <code>&#39;use strict&#39;</code> 标明代码使用 JavaScript 的严格模式，在严格模式下可以消除 Javascript 语法的一些不合理、不严谨之处，减少一些怪异行为，如不能直接使用未声明的变量，这样可以保证代码的运行安全，同时提高编译器的效率，提高运行速度。在下方 <code>// Your code here...</code> 这里我们就可以编写自己的代码了。</p>
                  <h2 id="实战-JavaScript-逆向"><a href="#实战-JavaScript-逆向" class="headerlink" title="实战 JavaScript 逆向"></a>实战 JavaScript 逆向</h2>
                  <p>下面我们来通过一个简单的 JavaScript 逆向案例来演示一下 Tampermonkey 的作用。 在 JavaScript 逆向的时候，我们经常会需要追踪某些方法的堆栈调用情况，但很多情况下，一些 JavaScript 的变量或者方法名经过混淆之后是非常难以捕捉的。 但如果我们能掌握一定的门路或规律，辅助以 Tampermonkey，就可以更轻松地找出一些 JavaScript 方法的断点位置，从而加速逆向过程。 在逆向过程中，一个非常典型的技术就是 Hook 技术。Hook 技术中文又叫做钩子技术，它就是在程序运行的过程中，对其中的某个方法进行重写，在原先的方法前后加入我们自定义的代码。相当于在系统没有调用该函数之前，钩子程序就先捕获该消息，钩子函数先得到控制权，这时钩子函数既可以加工处理（改变）该函数的执行行为，还可以强制结束消息的传递。 如果觉得比较抽象，看完下面的 Hook 案例就懂了。 例如，我们接下来使用 Tampermonkey 实现对某个 JavaScript 方法的 Hook，轻松找到某个方法执行的位置，从而快速定位到逆向入口。 接下来我们来这么一个简单的网站：<a href="https://scrape.cuiqingcai.com/login1，这个网站结构非常简单，就是一个用户名密码登录，但是不同的是，点击" target="_blank" rel="noopener">https://scrape.cuiqingcai.com/login1，这个网站结构非常简单，就是一个用户名密码登录，但是不同的是，点击</a> Submit 的时候，表单提交 POST 的内容并不是单纯的用户名和密码，而是一个加密后的 Token。 页面长这样： <img src="https://qiniu.cuiqingcai.com/2019-12-14-163945.png" alt=""> 我们随便输入用户名密码，点击登录按钮，观察一下网络请求的变化。 可以看到如下结果： <img src="https://qiniu.cuiqingcai.com/2019-12-14-165609.png" alt=""> 看到实际上控制台提交了一个 POST 请求，内容为：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"token"</span>:<span class="string">"eyJ1c2VybmFtZSI6ImFkbWluIiwicGFzc3dvcmQiOiJhZG1pbiJ9"</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>嗯，确实，没有诸如 username 和 password 的内容了，那这究竟是个啥？我要是做爬虫的话？怎么模拟登录呢？ 模拟登录的前提当然就是找到当前 token 生成的逻辑了，那么问题来了，到底这个 token 和用户名、密码什么关系呢？我们怎么来找寻其中的蛛丝马迹呢？ 这里我们就可能思考了，本身输入的是用户名和密码，但是提交的时候却变成了一个 token，经过观察 token 的内容还很像 Base64 编码。这就代表，网站可能首先将用户名密码混为了一个新的字符串，然后最后经过了一次 Base64 编码，最后将其赋值为 token 来提交了。所以，初步观察我们可以得出这么多信息。 好，那就来验证下吧，看看网站 JavaScript 代码里面是咋实现的。 接下来我们看看网站的源码，打开 Sources 面板，好家伙，看起来都是 Webpack 打包之后的内容，经过了一些混淆，类似结果如下： <img src="https://qiniu.cuiqingcai.com/2019-12-14-171158.png" alt=""> 这么多混淆代码，总不能一点点扒着看吧？这得找到猴年马月？那么遇到这种情形，这怎么去找 token 的生成位置呢？ 解决方法其实有两种。</p>
                  <h3 id="Ajax-断点"><a href="#Ajax-断点" class="headerlink" title="Ajax 断点"></a>Ajax 断点</h3>
                  <p>由于这个请求正好是一个 Ajax 请求，所以我们可以添加一个 XHR 断点监听，把 POST 的网址加到断点监听上面去，在 Sources 面板右侧添加这么一个 XHR 断点，如图所示： <img src="https://qiniu.cuiqingcai.com/2019-12-14-171458.png" alt="image-20191215011457030"> 这时候如果我们再次点击登录按钮的时候，正好发起一次 Ajax 请求，就进入到断点了，然后再看堆栈信息就可以一步步找到编码的入口了。 点击 Submit 之后，页面就进入了 Debug 状态停下来了，结果如下： <img src="https://qiniu.cuiqingcai.com/2019-12-14-171736.png" alt="image-20191215011734985"> 一步步找，我们最后其实可以找到入口其实是在 onSubmit 方法这里。但实际上，我们观察到，这里的断点的栈顶还会包括了一些 async Promise 等无关的内容，而我们真正想找的是用户名和密码经过处理，再进行 Base64 编码的地方，这些请求的调用实际上和我们找寻的入口是没有很大的关系的。 另外，如果我们想找的入口位置并不伴随这一次 Ajax 请求，这个方法就没法用了。 这个方法是奏效的，但是我们先不讲 onSubmit 方法里面究竟是什么逻辑，下一个方法再来讲。</p>
                  <h3 id="Hook-Function"><a href="#Hook-Function" class="headerlink" title="Hook Function"></a>Hook Function</h3>
                  <p>所以，这里介绍第二种可以快速定位入口的方法，那就是使用 Tampermonkey 自定义 JavaScript 实现某个 JavaScript 方法的 Hook。Hook 哪里呢？最明显的，Hook Base64 编码的位置就好了。 那么这里就涉及到一个小知识点，JavaScript 里面的 Base64 编码是怎么实现的。没错就是 btoa 方法，所以说，我们来 Hook btoa 方法就好了。 好，这里我们新建一个 Tampermonkey 脚本，内容如下：</p>
                  <figure class="highlight javascript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment">// ==UserScript==</span></span><br><span class="line"><span class="comment">// @name         HookBase64</span></span><br><span class="line"><span class="comment">// @namespace    https://scrape.cuiqingcai.com/</span></span><br><span class="line"><span class="comment">// @version      0.1</span></span><br><span class="line"><span class="comment">// @description  Hook Base64 encode function</span></span><br><span class="line"><span class="comment">// @author       Germey</span></span><br><span class="line"><span class="comment">// @match       https://scrape.cuiqingcai.com/login1</span></span><br><span class="line"><span class="comment">// @grant        none</span></span><br><span class="line"><span class="comment">// ==/UserScript==</span></span><br><span class="line">(<span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line"><span class="meta">    'use strict'</span></span><br><span class="line">    <span class="function"><span class="keyword">function</span> <span class="title">hook</span>(<span class="params">object, attr</span>) </span>&#123;</span><br><span class="line">        <span class="keyword">var</span> func = object[attr]</span><br><span class="line">        object[attr] = <span class="function"><span class="keyword">function</span> (<span class="params"></span>) </span>&#123;</span><br><span class="line">            <span class="built_in">console</span>.log(<span class="string">'hooked'</span>, object, attr)</span><br><span class="line">            <span class="keyword">var</span> ret = func.apply(object, <span class="built_in">arguments</span>)</span><br><span class="line">            <span class="keyword">debugger</span></span><br><span class="line">            <span class="keyword">return</span> ret</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    hook(<span class="built_in">window</span>, <span class="string">'btoa'</span>)</span><br><span class="line">&#125;)()</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>首先我们定义了一些 UserScript Header，包括 @name、@match 等等，这里比较重要的就是 @name，表示脚本名称；另外一个就是 @match，代表脚本生效的网址。 脚本的内容如上所示。我们定义了一个 hook 方法，传入 object 和 attr 参数，意思就是 Hook object 对象的 attr 参数。例如我们如果想 Hook 一个 alert 方法，那就把 object 设置为 window，把 attr 设置为 alert 字符串。这里我们想要 Hook Base64 的编码方法，在 JavaScript 中，Based64 编码是用 btoa 方法实现的，那么这里我们就只需要 Hook window 对象的 btoa 方法就好了。 那么 Hook 是怎么实现的呢？我们来看下，首先一句 <code>var func = object[attr]</code>，相当于我们先把它赋值为一个变量，我们调用 func 方法就可以实现和原来相同的功能。接着，我们再直接改写这个方法的定义，直接改写 <code>object[attr]</code>，将其改写成一个新的方法，在新的方法中，通过 <code>func.apply</code> 方法又重新调用了原来的方法。这样我们就可以保证，前后方法的执行效果是不受什么影响的，之前这个方法该干啥就还是干啥的。但是和之前不同的是，我们自定义方法之后，现在可以在 <code>func</code> 方法执行的前后，再加入自己的代码，如 <code>console.log</code> 将信息输出到控制台，如 <code>debugger</code> 进入断点等等。这个过程中，我们先临时保存下来了 <code>func</code> 方法，然后定义一个新的方法，接管程序控制权，在其中自定义我们想要的实现，同时在新的方法里面再重新调回 <code>func</code> 方法，保证前后结果是不受影响的。所以，我们达到了在不影响原有方法效果的前提下，可以实现在方法的前后实现自定义的功能，就是 Hook 的过程。 最后，我们调用 hook 方法，传入 window 对象和 btoa 字符串，保存。 接下来刷新下页面，这时候我们就可以看到这个脚本就在当前页面生效了，如图所示。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-174022.png" alt=""> 接下来，打开控制台，切换到 Sources 面板，这时候我们可以看到站点下面的资源多了一个叫做 Tampermonkey 的目录，展开之后，发现就是我们刚刚自定义的脚本。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-174210.png" alt=""> 然后输入用户名、密码，点击提交。发现成功进入了断点模式停下来了，代码就卡在了我们自定义的 <code>debugger</code> 这一行代码的位置，如下图所示。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-174338.png" alt=""> 成功 Hook 住了，这说明 JavaScript 代码在执行过程中调用到了 btoa 方法。 看下控制台，如下图所示。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-174540.png" alt="image-20191215014538625"> 这里也输出了 window 对象和 btoa 方法，验证正确。 这样，我们就顺利找到了 Base64 编码操作这个路口，然后看看堆栈信息，也已经不会出现 async、Promise 这样的调用，很清晰地呈现了 btoa 方法逐层调用的过程，非常清晰明了了，如图所示。 <img src="https://qiniu.cuiqingcai.com/2019-12-14-174803.png" alt=""> 各个底层的 encode 方法略过，这样我们也非常顺利地找到了 onSubmit 方法里面的处理逻辑：</p>
                  <figure class="highlight javascript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">onSubmit: <span class="function"><span class="keyword">function</span>(<span class="params"></span>) </span>&#123;</span><br><span class="line">  <span class="keyword">var</span> e = c.encode(<span class="built_in">JSON</span>.stringify(<span class="keyword">this</span>.form));</span><br><span class="line">  <span class="keyword">this</span>.$http.post(a[<span class="string">"a"</span>].state.url.root, &#123;</span><br><span class="line">    token: e</span><br><span class="line">  &#125;).then((<span class="function"><span class="keyword">function</span>(<span class="params">e</span>) </span>&#123;</span><br><span class="line">    <span class="built_in">console</span>.log(<span class="string">"data"</span>, e)</span><br><span class="line">  &#125;))</span><br><span class="line">&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>仔细看看，encode 方法其实就是调用了一下 btoa 方法，就是一个 Base64 编码的过程。 另外堆栈信息中可以清晰地看到 Hook 的方法在执行前传入的参数值，即 arguments。另外执行的之后的结果 ret 也可以轻松地找到了，如图所示： <img src="https://qiniu.cuiqingcai.com/2019-12-23-211320.png" alt=""> 所以，现在我们知道了 token 和用户名、密码是什么关系了吧。 这里一目了然了，就是对表单进行了 JSON 序列化，然后调用了 encode 也就是 btoa 方法，并赋值为了 token，入口顺利解开。后面，我们只需要模拟这个过程就 OK 了。 所以，我们通过 Tampermonkey 自定义 JavaScript 脚本的方式实现了某个方法调用的 Hook，使得我们快速能定位到加密入口的位置，非常方便。 以后如果观察出来了一些门道，可以多使用这种方法来尝试，如 Hook encode 方法、decode 方法、stringify 方法、log 方法、alert 方法等等，简单而又高效。 以上便是通过 Tampermonkey 实现简单 Hook 的基础操作，当然这个仅仅是一个常见的基础案例，不过从中我们也可以总结出一些 Hook 的基本门道。 后面我们会继续介绍更多相关内容。</p>
                  <h2 id="参考来源"><a href="#参考来源" class="headerlink" title="参考来源"></a>参考来源</h2>
                  <ul>
                    <li>Hook 技术：<a href="https://www.jianshu.com/p/3382cc765b39" target="_blank" rel="noopener">https://www.jianshu.com/p/3382cc765b39</a></li>
                    <li>Tampermonkey：<a href="http://www.tampermonkey.net/" target="_blank" rel="noopener">http://www.tampermonkey.net/</a></li>
                    <li>Base64 编码：<a href="https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/atob" target="_blank" rel="noopener">https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/atob</a>。</li>
                    <li>示例网址：<a href="https://github.com/Python3WebSpider/Scrape" target="_blank" rel="noopener">https://github.com/Python3WebSpider/Scrape</a></li>
                  </ul>
                  <h2 id="注明"><a href="#注明" class="headerlink" title="注明"></a>注明</h2>
                  <p>本篇属于 JavaScript 逆向系列内容。由于某些原因，JavaScript 逆向是在爬虫中比较敏感的内容，因此文章中不会选取当前市面上任何一个商业网站作为案例，都是通过自建平台示例的方式来单独讲解某个知识点。另外大家不要将相关技术应用到非法用途，惜命惜命。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-24 05:47:08" itemprop="dateCreated datePublished" datetime="2019-12-24T05:47:08+08:00">2019-12-24</time>
                </span>
                <span id="/8602.html" class="post-meta-item leancloud_visitors" data-flag-title="如何通过 Tampermonkey 快速查找 JavaScript 加密入口" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>8.3k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>8 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8509.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8509.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 15.5–Gerapy 分布式管理</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="15-5-Gerapy-分布式管理"><a href="#15-5-Gerapy-分布式管理" class="headerlink" title="15.5 Gerapy 分布式管理"></a>15.5 Gerapy 分布式管理</h1>
                  <p>我们可以通过 Scrapyd-Client 将 Scrapy 项目部署到 Scrapyd 上，并且可以通过 Scrapyd API 来控制 Scrapy 的运行。那么，我们是否可以做到更优化？方法是否可以更方便可控？ 我们重新分析一下当前可以优化的问题。</p>
                  <ul>
                    <li>使用 Scrapyd-Client 部署时，需要在配置文件中配置好各台主机的地址，然后利用命令行执行部署过程。如果我们省去各台主机的地址配置，将命令行对接图形界面，只需要点击按钮即可实现批量部署，这样就更方便了。</li>
                    <li>使用 Scrapyd API 可以控制 Scrapy 任务的启动、终止等工作，但很多操作还是需要代码来实现，同时获取爬取日志还比较烦琐。如果我们有一个图形界面，只需要点击按钮即可启动和终止爬虫任务，同时还可以实时查看爬取日志报告，那这将大大节省我们的时间和精力。</li>
                  </ul>
                  <p>所以我们的终极目标是如下内容。</p>
                  <ul>
                    <li>更方便地控制爬虫运行</li>
                    <li>更直观地查看爬虫状态</li>
                    <li>更实时地查看爬取结果</li>
                    <li>更简单地实现项目部署</li>
                    <li>更统一地实现主机管理</li>
                  </ul>
                  <p>而这所有的工作均可通过 Gerapy 来实现。 Gerapy 是一个基于 Scrapyd、Scrapyd API、Django、Vue.js 搭建的分布式爬虫管理框架。接下来将简单介绍它的使用方法。</p>
                  <h3 id="1-准备工作"><a href="#1-准备工作" class="headerlink" title="1. 准备工作"></a>1. 准备工作</h3>
                  <p>在本节开始之前请确保已经正确安装好了 Gerapy，安装方式可以参考第一章。</p>
                  <h3 id="2-使用说明"><a href="#2-使用说明" class="headerlink" title="2. 使用说明"></a>2. 使用说明</h3>
                  <p>首先可以利用 gerapy 命令新建一个项目，命令如下：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">gerapy init</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样会在当前目录下生成一个 gerapy 文件夹，然后进入 gerapy 文件夹，会发现一个空的 projects 文件夹，我们后文会提及。 这时先对数据库进行初始化：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">gerapy migrate</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样即会生成一个 SQLite 数据库，数据库中会用于保存各个主机配置信息、部署版本等。 接下来启动 Gerapy 服务，命令如下：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">gerapy runserver</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样即可在默认 8000 端口上开启 Gerapy 服务，我们浏览器打开：<a href="http://localhost:8000" target="_blank" rel="noopener">http://localhost:8000</a> 即可进入 Gerapy 的管理页面，在这里提供了主机管理和项目管理的功能。 主机管理中，我们可以将各台主机的 Scrapyd 运行地址和端口添加，并加以名称标记，添加之后便会出现在主机列表中，Gerapy 会监控各台主机的运行状况并以不同的状态标识，如图 15-6 所示： <img src="./assets/15-6.jpg" alt=""> 图 15-6 主机列表 另外刚才我们提到在 gerapy 目录下有一个空的 projects 文件夹，这就是存放 Scrapy 目录的文件夹，如果我们想要部署某个 Scrapy 项目，只需要将该项目文件放到 projects 文件夹下即可。 比如这里我放了两个 Scrapy 项目，如图 15-7 所示： <img src="./assets/15-7.jpg" alt=""> 图 15-7 项目目录 这时重新回到 Gerapy 管理界面，点击项目管理，即可看到当前项目列表，如图 15-8 所示： <img src="./assets/15-8.jpg" alt=""> 图 15-8 项目列表 由于此处我有过打包和部署记录，在这里分别予以显示。 Gerapy 提供了项目在线编辑功能，我们可以点击编辑即可可视化地对项目进行编辑，如图 15-9 所示： <img src="./assets/15-9.jpg" alt=""> 图 15-9 可视化编辑 如果项目没有问题，可以点击部署进行打包和部署，部署之前需要打包项目，打包时可以指定版本描述，如图 15-10 所示： <img src="./assets/15-10.jpg" alt=""> 图 15-10 项目打包 打包完成之后可以直接点击部署按钮即可将打包好的 Scrapy 项目部署到对应的云主机上，同时也可以批量部署，如图 15-11 所示： <img src="./assets/15-11.jpg" alt=""> 图 15-11 部署页面 部署完毕之后就可以回到主机管理页面进行任务调度了，点击调度即可查看进入任务管理页面，可以当前主机所有任务的运行状态，如图 15-12 所示： <img src="./assets/15-12.jpg" alt=""> 图 15-12 任务运行状态 我们可以通过点击新任务、停止等按钮来实现任务的启动和停止等操作，同时也可以通过展开任务条目查看日志详情，如图 15-13 所示： <img src="./assets/15-13.jpg" alt=""> 图 15-13 查看日志 这样我们就可以实时查看到各个任务运行状态了。 以上便是 Gerapy 的一些功能的简单介绍，使用它我们可以更加方便地管理、部署和监控 Scrapy 项目，尤其是对分布式爬虫来说。 更多的信息可以查看 Gerapy 的 GitHub 地址：<a href="https://github.com/Gerapy" target="_blank" rel="noopener">https://github.com/Gerapy</a>。</p>
                  <h3 id="3-结语"><a href="#3-结语" class="headerlink" title="3. 结语"></a>3. 结语</h3>
                  <p>本节我们介绍了 Gerapy 的简单使用，利用它我们可以方便地实现 Scrapy 项目的部署、管理等操作，可以大大提高效率。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-12 09:28:36" itemprop="dateCreated datePublished" datetime="2019-12-12T09:28:36+08:00">2019-12-12</time>
                </span>
                <span id="/8509.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 15.5–Gerapy 分布式管理" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>1.7k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8506.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8506.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 15.4–Scrapyd 批量部署</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="15-4-Scrapyd-批量部署"><a href="#15-4-Scrapyd-批量部署" class="headerlink" title="15.4 Scrapyd 批量部署"></a>15.4 Scrapyd 批量部署</h1>
                  <p>我们在上一节实现了 Scrapyd 和 Docker 的对接，这样每台主机就不用再安装 Python 环境和安装 Scrapyd 了，直接执行一句 Docker 命令运行 Scrapyd 服务即可。但是这种做法有个前提，那就是每台主机都安装 Docker，然后再去运行 Scrapyd 服务。如果我们需要部署 10 台主机的话，工作量确实不小。 一种方案是，一台主机已经安装好各种开发环境，我们取到它的镜像，然后用镜像来批量复制多台主机，批量部署就可以轻松实现了。 另一种方案是，我们在新建主机的时候直接指定一个运行脚本，脚本里写好配置各种环境的命令，指定其在新建主机的时候自动执行，那么主机创建之后所有的环境就按照自定义的命令配置好了，这样也可以很方便地实现批量部署。 目前很多服务商都提供云主机服务，如阿里云、腾讯云、Azure、Amazon 等，不同的服务商提供了不同的批量部署云主机的方式。例如，腾讯云提供了创建自定义镜像的服务，在新建主机的时候使用自定义镜像创建新的主机即可，这样就可以批量生成多个相同的环境。Azure 提供了模板部署的服务，我们可以在模板中指定新建主机时执行的配置环境的命令，这样在主机创建之后环境就配置完成了。 本节我们就来看看这两种批量部署的方式，来实现 Docker 和 Scrapyd 服务的批量部署。</p>
                  <h3 id="1-镜像部署"><a href="#1-镜像部署" class="headerlink" title="1. 镜像部署"></a>1. 镜像部署</h3>
                  <p>以腾讯云为例进行说明。首先需要有一台已经安装好环境的云主机，Docker 和 Scrapyd 镜像均已经正确安装，Scrapyd 镜像启动加到开机启动脚本中，可以在开机时自动启动。 接下来我们来看下腾讯云下批量部署相同云服务的方法。 首先进入到腾讯云后台，可以点击更多选项制作镜像，如图 15-3 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-29-114145.png" alt=""> 图 15-3 制作镜像 然后输入镜像的一些配置信息，如图 15-4 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-29-114152.jpg" alt=""> 图 15-4 镜像配置 最后确认制作镜像即可，稍等片刻即可制作成功。 接下来我们可以创建新的主机，在新建主机时选择已经制作好的镜像即可，如图 15-5 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-29-114200.png" alt=""> 图 15-5 新建主机 后续配置过程按照提示进行即可。 配置完成之后登录新到云主机，即可看到当前主机 Docker 和 Scrapyd 镜像都已经安装好，Scrapyd 服务已经正常运行。 我们就通过自定义镜像的方式实现了相同环境的云主机的批量部署。</p>
                  <h3 id="2-模板部署"><a href="#2-模板部署" class="headerlink" title="2. 模板部署"></a>2. 模板部署</h3>
                  <p>Azure 的云主机在部署时都会使用一个部署模板，这个模板实际上是一个 JSON 文件，里面包含了很多部署时的配置选项，如主机名称、用户名、密码、主机型号等。在模板中我们可以指定新建完云主机之后执行的命令行脚本，如安装 Docker、运行镜像等。等部署工作全部完成之后，新创建的云主机就已经完成环境配置，同时运行相关服务。 这里提供一个部署 Linux 主机时自动安装 Docker 和运行 Scrapyd 镜像的模板，模板内容太多，源文件可以查看：<a href="https://github.com/Python3WebSpider/ScrapydDeploy/blob/master/azuredeploy.json。模板中" target="_blank" rel="noopener">https://github.com/Python3WebSpider/ScrapydDeploy/blob/master/azuredeploy.json。模板中</a> Microsoft.Compute/virtualMachines/extensions 部分有一个 commandToExecute 字段，它可以指定建立主机后自动执行的命令。这里的命令完成的是安装 Docker 并运行 Scrapyd 镜像服务的过程。 首先安装一个 Azure 组件，安装过程可以参考：<a href="https://docs.azure.cn/zh-cn/xplat-cli-install。之后就可以使用" target="_blank" rel="noopener">https://docs.azure.cn/zh-cn/xplat-cli-install。之后就可以使用</a> azure 命令行进行部署。 登录 Azure，这里登录的是中国区，命令如下：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">azure login -e AzureChinaCloud</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>如果没有资源组的话需要新建一个资源组，命令如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">azure<span class="built_in"> group </span>create myResourceGroup chinanorth</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>其中 myResourceGroup 就是资源组的名称，可以自行定义。 接下来就可以使用该模板进行部署了，命令如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">azure<span class="built_in"> group </span>deployment create --template-file azuredeploy.json myResourceGroup myDeploymentName</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里 myResourceGroup 就是资源组的名称，myDeploymentName 是部署任务的名称。 例如，部署一台 Linux 主机的过程如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">azure<span class="built_in"> group </span>deployment create --template-file azuredeploy.json MyResourceGroup SingleVMDeploy</span><br><span class="line">info:    Executing command<span class="built_in"> group </span>deployment create</span><br><span class="line">info:    Supply values <span class="keyword">for</span> the following parameters</span><br><span class="line">adminUsername:  datacrawl</span><br><span class="line">adminPassword:  DataCrawl123</span><br><span class="line">vmSize:  Standard_D2_v2</span><br><span class="line">vmName:  datacrawl-vm</span><br><span class="line">dnsLabelPrefix:  datacrawlvm</span><br><span class="line">storageAccountName:  datacrawlstorage</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行命令后会提示输入各个配置参数，如主机用户名、密码等。之后等待整个部署工作完成即可，命令行会自动退出。然后，我们登录云主机即可查看到 Docker 已经成功安装并且 Scrapyd 服务正常运行。</p>
                  <h3 id="3-结语"><a href="#3-结语" class="headerlink" title="3. 结语"></a>3. 结语</h3>
                  <p>以上内容便是批量部署的两种方法。在大规模分布式爬虫架构中，如果需要批量部署多个爬虫环境，使用如上方法可以快速批量完成环境的搭建工作，而不用再去逐个主机配置环境。 到此为止，我们解决了批量部署的问题，创建主机完毕之后即可直接使用 Scrapyd 服务。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-12 09:26:43" itemprop="dateCreated datePublished" datetime="2019-12-12T09:26:43+08:00">2019-12-12</time>
                </span>
                <span id="/8506.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 15.4–Scrapyd 批量部署" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>2.3k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8494.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8494.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 15.3–Scrapyd 对接 Docker</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="15-3-Scrapyd-对接-Docker"><a href="#15-3-Scrapyd-对接-Docker" class="headerlink" title="15.3 Scrapyd 对接 Docker"></a>15.3 Scrapyd 对接 Docker</h1>
                  <p>我们使用了 Scrapyd-Client 成功将 Scrapy 项目部署到 Scrapyd 运行，前提是需要提前在服务器上安装好 Scrapyd 并运行 Scrapyd 服务，而这个过程比较麻烦。如果同时将一个 Scrapy 项目部署到 100 台服务器上，我们需要手动配置每台服务器的 Python 环境，更改 Scrapyd 配置吗？如果这些服务器的 Python 环境是不同版本，同时还运行其他的项目，而版本冲突又会造成不必要的麻烦。 所以，我们需要解决一个痛点，那就是 Python 环境配置问题和版本冲突解决问题。如果我们将 Scrapyd 直接打包成一个 Docker 镜像，那么在服务器上只需要执行 Docker 命令就可以启动 Scrapyd 服务，这样就不用再关心 Python 环境问题，也不需要担心版本冲突问题。 接下来，我们就将 Scrapyd 打包制作成一个 Docker 镜像。</p>
                  <h3 id="1-准备工作"><a href="#1-准备工作" class="headerlink" title="1. 准备工作"></a>1. 准备工作</h3>
                  <p>请确保本机已经正确安装好了 Docker，如没有安装可以参考第 1 章的安装说明。</p>
                  <h3 id="2-对接-Docker"><a href="#2-对接-Docker" class="headerlink" title="2. 对接 Docker"></a>2. 对接 Docker</h3>
                  <p>接下来我们首先新建一个项目，然后新建一个 scrapyd.conf，即 Scrapyd 的配置文件，内容如下：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="section">[scrapyd]</span></span><br><span class="line"><span class="attr">eggs_dir</span>    = eggs</span><br><span class="line"><span class="attr">logs_dir</span>    = logs</span><br><span class="line"><span class="attr">items_dir</span>   =</span><br><span class="line"><span class="attr">jobs_to_keep</span> = <span class="number">5</span></span><br><span class="line"><span class="attr">dbs_dir</span>     = dbs</span><br><span class="line"><span class="attr">max_proc</span>    = <span class="number">0</span></span><br><span class="line"><span class="attr">max_proc_per_cpu</span> = <span class="number">10</span></span><br><span class="line"><span class="attr">finished_to_keep</span> = <span class="number">100</span></span><br><span class="line"><span class="attr">poll_interval</span> = <span class="number">5.0</span></span><br><span class="line"><span class="attr">bind_address</span> = <span class="number">0.0</span>.<span class="number">0.0</span></span><br><span class="line"><span class="attr">http_port</span>   = <span class="number">6800</span></span><br><span class="line"><span class="attr">debug</span>       = <span class="literal">off</span></span><br><span class="line"><span class="attr">runner</span>      = scrapyd.runner</span><br><span class="line"><span class="attr">application</span> = scrapyd.app.application</span><br><span class="line"><span class="attr">launcher</span>    = scrapyd.launcher.Launcher</span><br><span class="line"><span class="attr">webroot</span>     = scrapyd.website.Root</span><br><span class="line"></span><br><span class="line"><span class="section">[services]</span></span><br><span class="line"><span class="attr">schedule.json</span>     = scrapyd.webservice.Schedule</span><br><span class="line"><span class="attr">cancel.json</span>       = scrapyd.webservice.Cancel</span><br><span class="line"><span class="attr">addversion.json</span>   = scrapyd.webservice.AddVersion</span><br><span class="line"><span class="attr">listprojects.json</span> = scrapyd.webservice.ListProjects</span><br><span class="line"><span class="attr">listversions.json</span> = scrapyd.webservice.ListVersions</span><br><span class="line"><span class="attr">listspiders.json</span>  = scrapyd.webservice.ListSpiders</span><br><span class="line"><span class="attr">delproject.json</span>   = scrapyd.webservice.DeleteProject</span><br><span class="line"><span class="attr">delversion.json</span>   = scrapyd.webservice.DeleteVersion</span><br><span class="line"><span class="attr">listjobs.json</span>     = scrapyd.webservice.ListJobs</span><br><span class="line"><span class="attr">daemonstatus.json</span> = scrapyd.webservice.DaemonStatus</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里实际上是修改自官方文档的配置文件：<a href="https://scrapyd.readthedocs.io/en/stable/config.html#example-configuration-file" target="_blank" rel="noopener">https://scrapyd.readthedocs.io/en/stable/config.html#example-configuration-file</a>，其中修改的地方有两个：</p>
                  <ul>
                    <li>max_proc_per_cpu = 10，原本是 4，即 CPU 单核最多运行 4 个 Scrapy 任务，也就是说 1 核的主机最多同时只能运行 4 个 Scrapy 任务，在这里设置上限为 10，也可以自行设置。</li>
                    <li>bind_address = 0.0.0.0，原本是 127.0.0.1，不能公开访问，在这里修改为 0.0.0.0 即可解除此限制。</li>
                  </ul>
                  <p>接下来新建一个 requirements.txt ，将一些 Scrapy 项目常用的库都列进去，内容如下：</p>
                  <figure class="highlight mipsasm">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">requests</span><br><span class="line">selenium</span><br><span class="line">aiohttp</span><br><span class="line"><span class="keyword">beautifulsoup4</span></span><br><span class="line"><span class="keyword">pyquery</span></span><br><span class="line"><span class="keyword">pymysql</span></span><br><span class="line"><span class="keyword">redis</span></span><br><span class="line"><span class="keyword">pymongo</span></span><br><span class="line"><span class="keyword">flask</span></span><br><span class="line"><span class="keyword">django</span></span><br><span class="line"><span class="keyword">scrapy</span></span><br><span class="line"><span class="keyword">scrapyd</span></span><br><span class="line"><span class="keyword">scrapyd-client</span></span><br><span class="line"><span class="keyword">scrapy-redis</span></span><br><span class="line"><span class="keyword">scrapy-splash</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>如果我们运行的 Scrapy 项目还有其他的库需要用到可以自行添加到此文件中。 最后我们新建一个 Dockerfile，内容如下：</p>
                  <figure class="highlight dockerfile">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">FROM</span> python:<span class="number">3.6</span></span><br><span class="line"><span class="keyword">ADD</span><span class="bash"> . /code</span></span><br><span class="line"><span class="keyword">WORKDIR</span><span class="bash"> /code</span></span><br><span class="line"><span class="keyword">COPY</span><span class="bash"> ./scrapyd.conf /etc/scrapyd/</span></span><br><span class="line"><span class="keyword">EXPOSE</span> <span class="number">6800</span></span><br><span class="line"><span class="keyword">RUN</span><span class="bash"> pip3 install -r requirements.txt</span></span><br><span class="line"><span class="keyword">CMD</span><span class="bash"> scrapyd</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>第一行 FROM 是指在 python:3.6 这个镜像上构建，也就是说在构建时就已经有了 Python 3.6 的环境。 第二行 ADD 是将本地的代码放置到虚拟容器中，它有两个参数，第一个参数是 . ，即代表本地当前路径，/code 代表虚拟容器中的路径，也就是将本地项目所有内容放置到虚拟容器的 /code 目录下。 第三行 WORKDIR 是指定工作目录，在这里将刚才我们添加的代码路径设成工作路径，在这个路径下的目录结构和我们当前本地目录结构是相同的，所以可以直接执行库安装命令等。 第四行 COPY 是将当前目录下的 scrapyd.conf 文件拷贝到虚拟容器的 /etc/scrapyd/ 目录下，Scrapyd 在运行的时候会默认读取这个配置。 第五行 EXPOSE 是声明运行时容器提供服务端口，注意这里只是一个声明，在运行时不一定就会在此端口开启服务。这样的声明一是告诉使用者这个镜像服务的运行端口，以方便配置映射。另一个用处则是在运行时使用随机端口映射时，会自动随机映射 EXPOSE 的端口。 第六行 RUN 是执行某些命令，一般做一些环境准备工作，由于 Docker 虚拟容器内只有 Python3 环境，而没有我们所需要的一些 Python 库，所以在这里我们运行此命令来在虚拟容器中安装相应的 Python 库，这样项目部署到 Scrapyd 中便可以正常运行了。 第七行 CMD 是容器启动命令，在容器运行时，会直接执行此命令，在这里我们直接用 scrapyd 来启动 Scrapyd 服务。 到现在基本的工作就完成了，运行如下命令进行构建：</p>
                  <figure class="highlight mipsasm">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">docker <span class="keyword">build </span>-t <span class="keyword">scrapyd:latest </span>.</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>构建成功后即可运行测试：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">docker run -d -p <span class="number">6800</span>:<span class="number">6800</span> scrapyd</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行之后我们打开：<a href="http://localhost:6800" target="_blank" rel="noopener">http://localhost:6800</a> 即可观察到 Scrapyd 服务，如图 15-2 所示： <img src="https://qiniu.cuiqingcai.com/2019-11-29-114136.png" alt=""> 图 15-2 Scrapyd 主页 这样我们就完成了 Scrapyd Docker 镜像的构建并成功运行了。 然后我们可以将此镜像上传到 Docker Hub，例如我的 Docker Hub 用户名为 germey，新建了一个名为 scrapyd 的项目，首先可以打一个标签：</p>
                  <figure class="highlight crmsh">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">docker <span class="keyword">tag</span> <span class="title">scrapyd</span>:latest germey/scrapyd:latest</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里请自行替换成你的项目名称。 然后 Push 即可：</p>
                  <figure class="highlight armasm">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="symbol">docker</span> <span class="keyword">push </span>germey/scrapyd:latest</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>之后我们在其他主机运行此命令即可启动 Scrapyd 服务：</p>
                  <figure class="highlight angelscript">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">docker run -d -p <span class="number">6800</span>:<span class="number">6800</span> germey/scrapyd</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>执行命令后会发现 Scrapyd 就可以成功在其他服务器上运行了。</p>
                  <h3 id="3-结语"><a href="#3-结语" class="headerlink" title="3. 结语"></a>3. 结语</h3>
                  <p>这样我们就利用 Docker 解决了 Python 环境的问题，在后一节我们再解决一个批量部署 Docker 的问题就可以解决批量部署问题了。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-11 10:13:48" itemprop="dateCreated datePublished" datetime="2019-12-11T10:13:48+08:00">2019-12-11</time>
                </span>
                <span id="/8494.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 15.3–Scrapyd 对接 Docker" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>3.1k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>3 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8491.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8491.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 15.2–Scrapyd-Client 的使用</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="15-2-Scrapyd-Client-的使用"><a href="#15-2-Scrapyd-Client-的使用" class="headerlink" title="15.2 Scrapyd-Client 的使用"></a>15.2 Scrapyd-Client 的使用</h1>
                  <p>这里有现成的工具来完成部署过程，它叫作 Scrapyd-Client。本节将简单介绍使用 Scrapyd-Client 部署 Scrapy 项目的方法。</p>
                  <h3 id="1-准备工作"><a href="#1-准备工作" class="headerlink" title="1. 准备工作"></a>1. 准备工作</h3>
                  <p>请先确保 Scrapyd-Client 已经正确安装，安装方式可以参考第 1 章的内容。</p>
                  <h3 id="2-Scrapyd-Client-的功能"><a href="#2-Scrapyd-Client-的功能" class="headerlink" title="2. Scrapyd-Client 的功能"></a>2. Scrapyd-Client 的功能</h3>
                  <p>Scrapyd-Client 为了方便 Scrapy 项目的部署，提供两个功能：</p>
                  <ul>
                    <li>将项目打包成 Egg 文件。</li>
                    <li>将打包生成的 Egg 文件通过 addversion.json 接口部署到 Scrapyd 上。</li>
                  </ul>
                  <p>也就是说，Scrapyd-Client 帮我们把部署全部实现了，我们不需要再去关心 Egg 文件是怎样生成的，也不需要再去读 Egg 文件并请求接口上传了，这一切的操作只需要执行一个命令即可一键部署。</p>
                  <h3 id="3-Scrapyd-Client-部署"><a href="#3-Scrapyd-Client-部署" class="headerlink" title="3. Scrapyd-Client 部署"></a>3. Scrapyd-Client 部署</h3>
                  <p>要部署 Scrapy 项目，我们首先需要修改一下项目的配置文件，例如我们之前写的 Scrapy 微博爬虫项目，在项目的第一层会有一个 scrapy.cfg 文件，它的内容如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">[settings]</span><br><span class="line">default = weibo.settings</span><br><span class="line"></span><br><span class="line">[deploy]</span><br><span class="line"><span class="comment">#url = http://localhost:6800/</span></span><br><span class="line">project = weibo</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里我们需要配置一下 deploy 部分，例如我们要将项目部署到 120.27.34.25 的 Scrapyd 上，就需要修改为如下内容：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="section">[deploy]</span></span><br><span class="line"><span class="attr">url</span> = http://<span class="number">120.27</span>.<span class="number">34.25</span>:<span class="number">6800</span>/</span><br><span class="line"><span class="attr">project</span> = weibo</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们再在 scrapy.cfg 文件所在路径执行如下命令：</p>
                  <figure class="highlight ebnf">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attribute">scrapyd-deploy</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>运行结果如下：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">Packing <span class="keyword">version</span> <span class="number">1501682277</span></span><br><span class="line">Deploying <span class="keyword">to</span> project "weibo" <span class="keyword">in</span> http://<span class="number">120.27</span><span class="number">.34</span><span class="number">.25</span>:<span class="number">6800</span>/addversion.json</span><br><span class="line"><span class="keyword">Server</span> response (<span class="number">200</span>):</span><br><span class="line">&#123;"status": "ok", "spiders": <span class="number">1</span>, "node_name": "datacrawl-vm", "project": "weibo", "version": "1501682277"&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>返回这样的结果就代表部署成功了。 我们也可以指定项目版本，如果不指定的话默认为当前时间戳，指定的话通过 version 参数传递即可，例如：</p>
                  <figure class="highlight ada">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">scrapyd-deploy <span class="comment">--version 201707131455</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>值得注意的是在 Python3 的 Scrapyd 1.2.0 版本中我们不要指定版本号为带字母的字符串，需要为纯数字，否则可能会出现报错。 另外如果我们有多台主机，我们可以配置各台主机的别名，例如可以修改配置文件为：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="section">[deploy:vm1]</span></span><br><span class="line"><span class="attr">url</span> = http://<span class="number">120.27</span>.<span class="number">34.24</span>:<span class="number">6800</span>/</span><br><span class="line"><span class="attr">project</span> = weibo</span><br><span class="line"></span><br><span class="line"><span class="section">[deploy:vm2]</span></span><br><span class="line"><span class="attr">url</span> = http://<span class="number">139.217</span>.<span class="number">26.30</span>:<span class="number">6800</span>/</span><br><span class="line"><span class="attr">project</span> = weibo</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>有多台主机的话就在此统一配置，一台主机对应一组配置，在 deploy 后面加上主机的别名即可，这样如果我们想将项目部署到 IP 为 139.217.26.30 的 vm2 主机，我们只需要执行如下命令：</p>
                  <figure class="highlight gcode">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">scrapyd-deploy v<span class="name">m2</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就可以将项目部署到名称为 vm2 的主机上了。 如此一来，如果我们有多台主机，我们只需要在 scrapy.cfg 文件中配置好各台主机的 Scrapyd 地址，然后调用 scrapyd-deploy 命令加主机名称即可实现部署，非常方便。 如果 Scrapyd 设置了访问限制的话，我们可以在配置文件中加入用户名和密码的配置，同时端口修改一下，修改成 Nginx 代理端口，如在第一章我们使用的是 6801，那么这里就需要改成 6801，修改如下：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="section">[deploy:vm1]</span></span><br><span class="line"><span class="attr">url</span> = http://<span class="number">120.27</span>.<span class="number">34.24</span>:<span class="number">6801</span>/</span><br><span class="line"><span class="attr">project</span> = weibo</span><br><span class="line"><span class="attr">username</span> = admin</span><br><span class="line"><span class="attr">password</span> = admin</span><br><span class="line"></span><br><span class="line"><span class="section">[deploy:vm2]</span></span><br><span class="line"><span class="attr">url</span> = http://<span class="number">139.217</span>.<span class="number">26.30</span>:<span class="number">6801</span>/</span><br><span class="line"><span class="attr">project</span> = weibo</span><br><span class="line"><span class="attr">username</span> = germey</span><br><span class="line"><span class="attr">password</span> = germey</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样通过加入 username 和 password 字段我们就可以在部署时自动进行 Auth 验证，然后成功实现部署。</p>
                  <h3 id="4-结语"><a href="#4-结语" class="headerlink" title="4. 结语"></a>4. 结语</h3>
                  <p>本节介绍了利用 Scrapyd-Client 来方便地将项目部署到 Scrapyd 的过程，有了它部署不再是麻烦事。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-11 09:55:47" itemprop="dateCreated datePublished" datetime="2019-12-11T09:55:47+08:00">2019-12-11</time>
                </span>
                <span id="/8491.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 15.2–Scrapyd-Client 的使用" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>1.9k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>2 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8475.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8475.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 15.1–Scrapyd 分布式部署</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="15-1-Scrapyd-分布式部署"><a href="#15-1-Scrapyd-分布式部署" class="headerlink" title="15.1 Scrapyd 分布式部署"></a>15.1 Scrapyd 分布式部署</h1>
                  <p>分布式爬虫完成并可以成功运行了，但是有个环节非常烦琐，那就是代码部署。 我们设想下面的几个场景。</p>
                  <ul>
                    <li>如果采用上传文件的方式部署代码，我们首先将代码压缩，然后采用 SFTP 或 FTP 的方式将文件上传到服务器，之后再连接服务器将文件解压，每个服务器都需要这样配置。</li>
                    <li>如果采用 Git 同步的方式部署代码，我们可以先把代码 Push 到某个 Git 仓库里，然后再远程连接各台主机执行 Pull 操作，同步代码，每个服务器同样需要做一次操作。</li>
                  </ul>
                  <p>如果代码突然有更新，那我们必须更新每个服务器，而且万一哪台主机的版本没控制好，这可能会影响整体的分布式爬取状况。 所以我们需要一个更方便的工具来部署 Scrapy 项目，如果可以省去一遍遍逐个登录服务器部署的操作，那将会方便很多。 本节我们就来看看提供分布式部署的工具 Scrapyd。</p>
                  <h3 id="1-了解-Scrapyd"><a href="#1-了解-Scrapyd" class="headerlink" title="1. 了解 Scrapyd"></a>1. 了解 Scrapyd</h3>
                  <p>Scrapyd 是一个运行 Scrapy 爬虫的服务程序，它提供一系列 HTTP 接口来帮助我们部署、启动、停止、删除爬虫程序。Scrapyd 支持版本管理，同时还可以管理多个爬虫任务，利用它我们可以非常方便地完成 Scrapy 爬虫项目的部署任务调度。</p>
                  <h3 id="2-准备工作"><a href="#2-准备工作" class="headerlink" title="2. 准备工作"></a>2. 准备工作</h3>
                  <p>请确保本机或服务器已经正确安装好了 Scrapyd，安装和配置的方法可以参见第 1 章的内容。</p>
                  <h3 id="3-访问-Scrapyd"><a href="#3-访问-Scrapyd" class="headerlink" title="3. 访问 Scrapyd"></a>3. 访问 Scrapyd</h3>
                  <p>安装并运行了 Scrapyd 之后，我们就可以访问服务器的 6800 端口看到一个 WebUI 页面了，例如我的服务器地址为 120.27.34.25，在上面安装好了 Scrapyd 并成功运行，那么我就可以在本地的浏览器中打开：<a href="http://120.27.34.25:6800" target="_blank" rel="noopener">http://120.27.34.25:6800</a>，就可以看到 Scrapyd 的首页，这里请自行替换成你的服务器地址查看即可，如图 15-1 所示： <img src="https://qiniu.cuiqingcai.com/2019-11-29-114054.png" alt=""> 图 15-1 Scrapyd 首页 如果可以成功访问到此页面，那么证明 Scrapyd 配置就没有问题了。</p>
                  <h3 id="4-Scrapyd-的功能"><a href="#4-Scrapyd-的功能" class="headerlink" title="4. Scrapyd 的功能"></a>4. Scrapyd 的功能</h3>
                  <p>Scrapyd 提供了一系列 HTTP 接口来实现各种操作，在这里我们可以将接口的功能梳理一下，以 Scrapyd 所在的 IP 为 120.27.34.25 为例：</p>
                  <h4 id="daemonstatus-json"><a href="#daemonstatus-json" class="headerlink" title="daemonstatus.json"></a>daemonstatus.json</h4>
                  <p>这个接口负责查看 Scrapyd 当前的服务和任务状态，我们可以用 curl 命令来请求这个接口，命令如下：</p>
                  <figure class="highlight groovy">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="string">http:</span><span class="comment">//139.217.26.30:6800/daemonstatus.json</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就会得到如下结果：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>, <span class="attr">"finished"</span>: <span class="number">90</span>, <span class="attr">"running"</span>: <span class="number">9</span>, <span class="attr">"node_name"</span>: <span class="string">"datacrawl-vm"</span>, <span class="attr">"pending"</span>: <span class="number">0</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>返回结果是 Json 字符串，status 是当前运行状态， finished 代表当前已经完成的 Scrapy 任务，running 代表正在运行的 Scrapy 任务，pending 代表等待被调度的 Scrapyd 任务，node_name 就是主机的名称。</p>
                  <h4 id="addversion-json"><a href="#addversion-json" class="headerlink" title="addversion.json"></a>addversion.json</h4>
                  <p>这个接口主要是用来部署 Scrapy 项目用的，在部署的时候我们需要首先将项目打包成 Egg 文件，然后传入项目名称和部署版本。 我们可以用如下的方式实现项目部署：</p>
                  <figure class="highlight livecodeserver">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="keyword">http</span>://<span class="number">120.27</span><span class="number">.34</span><span class="number">.25</span>:<span class="number">6800</span>/addversion.json -F project=wenbo -F <span class="built_in">version</span>=<span class="keyword">first</span> -F egg=@weibo.egg</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里 -F 即代表添加一个参数，同时我们还需要将项目打包成 Egg 文件放到本地。 这样发出请求之后我们可以得到如下结果：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>, <span class="attr">"spiders"</span>: <span class="number">3</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这个结果表明部署成功，并且其中包含的 Spider 的数量为 3。 此方法部署可能比较繁琐，在后文会介绍更方便的工具来实现项目的部署。</p>
                  <h4 id="schedule-json"><a href="#schedule-json" class="headerlink" title="schedule.json"></a>schedule.json</h4>
                  <p>这个接口负责调度已部署好的 Scrapy 项目运行。 我们可以用如下接口实现任务调度：</p>
                  <figure class="highlight groovy">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="string">http:</span><span class="comment">//120.27.34.25:6800/schedule.json -d project=weibo -d spider=weibocn</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里需要传入两个参数，project 即 Scrapy 项目名称，spider 即 Spider 名称。 返回结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>, <span class="attr">"jobid"</span>: <span class="string">"6487ec79947edab326d6db28a2d86511e8247444"</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>status 代表 Scrapy 项目启动情况，jobid 代表当前正在运行的爬取任务代号。</p>
                  <h4 id="cancel-json"><a href="#cancel-json" class="headerlink" title="cancel.json"></a>cancel.json</h4>
                  <p>这个接口可以用来取消某个爬取任务，如果这个任务是 pending 状态，那么它将会被移除，如果这个任务是 running 状态，那么它将会被终止。 我们可以用下面的命令来取消任务的运行：</p>
                  <figure class="highlight dns">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl http://<span class="number">120.27.34.25</span>:<span class="number">6800</span>/cancel.json -d project=weibo -d job=<span class="number">6487</span>ec79947edab326d6db28a2d865<span class="number">11e8247444</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里需要传入两个参数，project 即项目名称，job 即爬取任务代号。 返回结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>, <span class="attr">"prevstate"</span>: <span class="string">"running"</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>status 代表请求执行情况，prevstate 代表之前的运行状态。</p>
                  <h4 id="listprojects-json"><a href="#listprojects-json" class="headerlink" title="listprojects.json"></a>listprojects.json</h4>
                  <p>这个接口用来列出部署到 Scrapyd 服务上的所有项目描述。 我们可以用下面的命令来获取 Scrapyd 服务器上的所有项目描述：</p>
                  <figure class="highlight groovy">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="string">http:</span><span class="comment">//120.27.34.25:6800/listprojects.json</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这里不需要传入任何参数。 返回结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>, <span class="attr">"projects"</span>: [<span class="string">"weibo"</span>, <span class="string">"zhihu"</span>]&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>status 代表请求执行情况，projects 是项目名称列表。</p>
                  <h4 id="listversions-json"><a href="#listversions-json" class="headerlink" title="listversions.json"></a>listversions.json</h4>
                  <p>这个接口用来获取某个项目的所有版本号，版本号是按序排列的，最后一个条目是最新的版本号。 我们可以用如下命令来获取项目的版本号：</p>
                  <figure class="highlight groovy">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="string">http:</span><span class="comment">//120.27.34.25:6800/listversions.json?project=weibo</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里需要一个参数 project，就是项目的名称。 返回结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>, <span class="attr">"versions"</span>: [<span class="string">"v1"</span>, <span class="string">"v2"</span>]&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>status 代表请求执行情况，versions 是版本号列表。</p>
                  <h4 id="listspiders-json"><a href="#listspiders-json" class="headerlink" title="listspiders.json"></a>listspiders.json</h4>
                  <p>这个接口用来获取某个项目最新的一个版本的所有 Spider 名称。 我们可以用如下命令来获取项目的 Spider 名称：</p>
                  <figure class="highlight groovy">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="string">http:</span><span class="comment">//120.27.34.25:6800/listspiders.json?project=weibo</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里需要一个参数 project，就是项目的名称。 返回结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>, <span class="attr">"spiders"</span>: [<span class="string">"weibocn"</span>]&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>status 代表请求执行情况，spiders 是 Spider 名称列表。</p>
                  <h4 id="listjobs-json"><a href="#listjobs-json" class="headerlink" title="listjobs.json"></a>listjobs.json</h4>
                  <p>这个接口用来获取某个项目当前运行的所有任务详情。 我们可以用如下命令来获取所有任务详情：</p>
                  <figure class="highlight groovy">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="string">http:</span><span class="comment">//120.27.34.25:6800/listjobs.json?project=weibo</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里需要一个参数 project，就是项目的名称。 返回结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>,</span><br><span class="line"> <span class="attr">"pending"</span>: [&#123;<span class="attr">"id"</span>: <span class="string">"78391cc0fcaf11e1b0090800272a6d06"</span>, <span class="attr">"spider"</span>: <span class="string">"weibocn"</span>&#125;],</span><br><span class="line"> <span class="attr">"running"</span>: [&#123;<span class="attr">"id"</span>: <span class="string">"422e608f9f28cef127b3d5ef93fe9399"</span>, <span class="attr">"spider"</span>: <span class="string">"weibocn"</span>, <span class="attr">"start_time"</span>: <span class="string">"2017-07-12 10:14:03.594664"</span>&#125;],</span><br><span class="line"> <span class="attr">"finished"</span>: [&#123;<span class="attr">"id"</span>: <span class="string">"2f16646cfcaf11e1b0090800272a6d06"</span>, <span class="attr">"spider"</span>: <span class="string">"weibocn"</span>, <span class="attr">"start_time"</span>: <span class="string">"2017-07-12 10:14:03.594664"</span>, <span class="attr">"end_time"</span>: <span class="string">"2017-07-12 10:24:03.594664"</span>&#125;]&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>status 代表请求执行情况，pendings 代表当前正在等待的任务，running 代表当前正在运行的任务，finished 代表已经完成的任务。</p>
                  <h4 id="delversion-json"><a href="#delversion-json" class="headerlink" title="delversion.json"></a>delversion.json</h4>
                  <p>这个接口用来删除项目的某个版本。 我们可以用如下命令来删除项目版本：</p>
                  <figure class="highlight groovy">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="string">http:</span><span class="comment">//120.27.34.25:6800/delversion.json -d project=weibo -d version=v1</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里需要一个参数 project，就是项目的名称，还需要一个参数 version，就是项目的版本。 返回结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>status 代表请求执行情况，这样就代表删除成功了。</p>
                  <h4 id="delproject-json"><a href="#delproject-json" class="headerlink" title="delproject.json"></a>delproject.json</h4>
                  <p>这个接口用来删除某个项目。 我们可以用如下命令来删除某个项目：</p>
                  <figure class="highlight groovy">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">curl <span class="string">http:</span><span class="comment">//120.27.34.25:6800/delproject.json -d project=weibo</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里需要一个参数 project，就是项目的名称。 返回结果如下：</p>
                  <figure class="highlight json">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">&#123;<span class="attr">"status"</span>: <span class="string">"ok"</span>&#125;</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>status 代表请求执行情况，这样就代表删除成功了。 以上就是 Scrapyd 所有的接口，我们可以直接请求 HTTP 接口即可控制项目的部署、启动、运行等操作。</p>
                  <h3 id="5-ScrapydAPI-的使用"><a href="#5-ScrapydAPI-的使用" class="headerlink" title="5. ScrapydAPI 的使用"></a>5. ScrapydAPI 的使用</h3>
                  <p>以上的这些接口可能使用起来还不是很方便，没关系，还有一个 ScrapydAPI 库对这些接口又做了一层封装，其安装方式也可以参考第一章的内容。 下面我们来看下 ScrapydAPI 的使用方法，其实核心原理和 HTTP 接口请求方式并无二致，只不过用 Python 封装后使用更加便捷。 我们可以用如下方式建立一个 ScrapydAPI 对象：</p>
                  <figure class="highlight clean">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="keyword">from</span> scrapyd_api <span class="keyword">import</span> ScrapydAPI</span><br><span class="line">scrapyd = ScrapydAPI(<span class="string">'http://120.27.34.25:6800'</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>然后就可以调用它的方法来实现对应接口的操作了，例如部署的操作可以使用如下方式：</p>
                  <figure class="highlight sas">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">egg =<span class="meta"> open(</span><span class="string">'weibo.egg'</span>, <span class="string">'rb'</span>)</span><br><span class="line">scrapyd.add_versi<span class="meta">on(</span><span class="string">'weibo'</span>, <span class="string">'v1'</span>, egg)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这样我们就可以将项目打包为 Egg 文件，然后把本地打包的的 Egg 项目部署到远程 Scrapyd 了。 另外 ScrapydAPI 还实现了所有 Scrapyd 提供的 API 接口，名称都是相同的，参数也是相同的。 例如我们可以调用 list_projects() 方法即可列出 Scrapyd 中所有已部署的项目：</p>
                  <figure class="highlight css">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="selector-tag">scrapyd</span><span class="selector-class">.list_projects</span>()</span><br><span class="line"><span class="selector-attr">[<span class="string">'weibo'</span>, <span class="string">'zhihu'</span>]</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>另外还有其他的方法在此不再一一列举了，名称和参数都是相同的，更加详细的操作可以参考其官方文档：<a href="http://python-scrapyd-api.readthedocs.io/" target="_blank" rel="noopener">http://python-scrapyd-api.readthedocs.io/</a>。</p>
                  <h3 id="6-结语"><a href="#6-结语" class="headerlink" title="6. 结语"></a>6. 结语</h3>
                  <p>本节介绍了 Scrapyd 及 ScrapydAPI 的相关用法，我们可以通过它来部署项目，并通过 HTTP 接口来控制人物的运行，不过这里有一个不方便的地方就是部署过程，首先它需要打包 Egg 文件然后再上传，还是比较繁琐的，在下一节我们介绍一个更加方便的工具来完成部署过程。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-10 09:26:07" itemprop="dateCreated datePublished" datetime="2019-12-10T09:26:07+08:00">2019-12-10</time>
                </span>
                <span id="/8475.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 15.1–Scrapyd 分布式部署" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>4.7k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>4 分钟</span>
                </span>
              </div>
            </article>
            <article itemscope itemtype="http://schema.org/Article" class="post-block index" lang="zh-CN">
              <link itemprop="mainEntityOfPage" href="https://cuiqingcai.com/8472.html">
              <span hidden itemprop="author" itemscope itemtype="http://schema.org/Person">
                <meta itemprop="image" content="/images/avatar.png">
                <meta itemprop="name" content="崔庆才">
                <meta itemprop="description" content="崔庆才的个人站点，记录生活的瞬间，分享学习的心得。">
              </span>
              <span hidden itemprop="publisher" itemscope itemtype="http://schema.org/Organization">
                <meta itemprop="name" content="静觅">
              </span>
              <header class="post-header">
                <h2 class="post-title" itemprop="name headline">
                  <a class="label"> Python <i class="label-arrow"></i>
                  </a>
                  <a href="/8472.html" class="post-title-link" itemprop="url">[Python3网络爬虫开发实战] 14.4–Bloom Filter 的对接</a>
                </h2>
              </header>
              <div class="post-body" itemprop="articleBody">
                <div class="thumb">
                  <img itemprop="contentUrl" class="random">
                </div>
                <div class="excerpt">
                  <p>
                  <h1 id="14-4-Bloom-Filter-的对接"><a href="#14-4-Bloom-Filter-的对接" class="headerlink" title="14.4 Bloom Filter 的对接"></a>14.4 Bloom Filter 的对接</h1>
                  <p>首先回顾一下 Scrapy-Redis 的去重机制。Scrapy-Redis 将 Request 的指纹存储到了 Redis 集合中，每个指纹的长度为 40，例如 27adcc2e8979cdee0c9cecbbe8bf8ff51edefb61 就是一个指纹，它的每一位都是 16 进制数。 我们计算一下用这种方式耗费的存储空间。每个十六进制数占用 4 b，1 个指纹用 40 个十六进制数表示，占用空间为 20 B，1 万个指纹即占用空间 200 KB，1 亿个指纹占用 2 GB。当爬取数量达到上亿级别时，Redis 的占用的内存就会变得很大，而且这仅仅是指纹的存储。Redis 还存储了爬取队列，内存占用会进一步提高，更别说有多个 Scrapy 项目同时爬取的情况了。当爬取达到亿级别规模时，Scrapy-Redis 提供的集合去重已经不能满足我们的要求。所以我们需要使用一个更加节省内存的去重算法 Bloom Filter。</p>
                  <h3 id="1-了解-BloomFilter"><a href="#1-了解-BloomFilter" class="headerlink" title="1. 了解 BloomFilter"></a>1. 了解 BloomFilter</h3>
                  <p>Bloom Filter，中文名称叫作布隆过滤器，是 1970 年由 Bloom 提出的，它可以被用来检测一个元素是否在一个集合中。Bloom Filter 的空间利用效率很高，使用它可以大大节省存储空间。Bloom Filter 使用位数组表示一个待检测集合，并可以快速地通过概率算法判断一个元素是否存在于这个集合中。利用这个算法我们可以实现去重效果。 本节我们来了解 Bloom Filter 的基本算法，以及 Scrapy-Redis 中对接 Bloom Filter 的方法。</p>
                  <h3 id="2-BloomFilter-的算法"><a href="#2-BloomFilter-的算法" class="headerlink" title="2. BloomFilter 的算法"></a>2. BloomFilter 的算法</h3>
                  <p>在 Bloom Filter 中使用位数组来辅助实现检测判断。在初始状态下，我们声明一个包含 m 位的位数组，它的所有位都是 0，如图 14-7 所示。 <img src="https://qiniu.cuiqingcai.com/2019-11-29-113850.jpg" alt=""> 图 14-7 初始位数组 现在我们有了一个待检测集合，我们表示为 S={x1, x2, …, xn}，我们接下来需要做的就是检测一个 x 是否已经存在于集合 S 中。在 BloomFilter 算法中首先使用 k 个相互独立的、随机的哈希函数来将这个集合 S 中的每个元素 x1、x2、…、xn 映射到这个长度为 m 的位数组上，哈希函数得到的结果记作位置索引，然后将位数组该位置索引的位置 1。例如这里我们取 k 为 3，即有三个哈希函数，x1 经过三个哈希函数映射得到的结果分别为 1、4、8，x2 经过三个哈希函数映射得到的结果分别为 4、6、10，那么就会将位数组的 1、4、6、8、10 这五位置 1，如图 14-8 所示： <img src="https://qiniu.cuiqingcai.com/2019-11-29-114343.jpg" alt=""> 图 14-8 映射后位数组 这时如果再有一个新的元素 x，我们要判断 x 是否属于 S 这个集合，我们便会将仍然用 k 个哈希函数对 x 求映射结果，如果所有结果对应的位数组位置均为 1，那么我们就认为 x 属于 S 这个集合，否则如果有一个不为 1，则 x 不属于 S 集合。 例如一个新元素 x 经过三个哈希函数映射的结果为 4、6、8，对应的位置均为 1，则判断 x 属于 S 这个集合。如果结果为 4、6、7，7 对应的位置为 0，则判定 x 不属于 S 这个集合。 注意这里 m、n、k 满足的关系是 m&gt;nk，也就是说位数组的长度 m 要比集合元素 n 和哈希函数 k 的乘积还要大。 这样的判定方法很高效，但是也是有代价的，它可能把不属于这个集合的元素误认为属于这个集合，我们来估计一下它的错误率。当集合 S={x1, x2,…, xn} 的所有元素都被 k 个哈希函数映射到 m 位的位数组中时，这个位数组中某一位还是 0 的概率是： <img src="https://qiniu.cuiqingcai.com/2019-11-29-114353.jpg" alt=""> 因为哈希函数是随机的，所以任意一个哈希函数选中这一位的概率为 1/m，那么 1-1/m 就代表哈希函数一次没有选中这一位的概率，要把 S 完全映射到 m 位数组中，需要做 kn 次哈希运算，所以最后的概率就是 1-1/m 的 kn 次方。 一个不属于 S 的元素 x 如果要被误判定为在 S 中，那么这个概率就是 k 次哈希运算得到的结果对应的位数组位置都为 1，所以误判概率为： <img src="https://qiniu.cuiqingcai.com/2019-11-29-114430.jpg" alt=""> 根据： <img src="https://qiniu.cuiqingcai.com/2019-11-29-114441.jpg" alt=""> 可以将误判概率转化为： <img src="https://qiniu.cuiqingcai.com/2019-11-29-114445.jpg" alt=""> 在给定 m、n 时，可以求出使得 f 最小化的 k 值为： <img src="https://qiniu.cuiqingcai.com/2019-11-29-114452.jpg" alt=""> 在这里将误判概率归纳如下： 表 14-1　误判概率</p>
                  <p>m/n</p>
                  <p>最优 k</p>
                  <p>k=1</p>
                  <p>k=2</p>
                  <p>k=3</p>
                  <p>k=4</p>
                  <p>k=5</p>
                  <p>k=6</p>
                  <p>k=7</p>
                  <p>k=8</p>
                  <p>2</p>
                  <p>1.39</p>
                  <p>0.393</p>
                  <p>0.400</p>
                  <p>3</p>
                  <p>2.08</p>
                  <p>0.283</p>
                  <p>0.237</p>
                  <p>0.253</p>
                  <p>4</p>
                  <p>2.77</p>
                  <p>0.221</p>
                  <p>0.155</p>
                  <p>0.147</p>
                  <p>0.160</p>
                  <p>5</p>
                  <p>3.46</p>
                  <p>0.181</p>
                  <p>0.109</p>
                  <p>0.092</p>
                  <p>0.092</p>
                  <p>0.101</p>
                  <p>6</p>
                  <p>4.16</p>
                  <p>0.154</p>
                  <p>0.0804</p>
                  <p>0.0609</p>
                  <p>0.0561</p>
                  <p>0.0578</p>
                  <p>0.0638</p>
                  <p>7</p>
                  <p>4.85</p>
                  <p>0.133</p>
                  <p>0.0618</p>
                  <p>0.0423</p>
                  <p>0.0359</p>
                  <p>0.0347</p>
                  <p>0.0364</p>
                  <p>8</p>
                  <p>5.55</p>
                  <p>0.118</p>
                  <p>0.0489</p>
                  <p>0.0306</p>
                  <p>0.024</p>
                  <p>0.0217</p>
                  <p>0.0216</p>
                  <p>0.0229</p>
                  <p>9</p>
                  <p>6.24</p>
                  <p>0.105</p>
                  <p>0.0397</p>
                  <p>0.0228</p>
                  <p>0.0166</p>
                  <p>0.0141</p>
                  <p>0.0133</p>
                  <p>0.0135</p>
                  <p>0.0145</p>
                  <p>10</p>
                  <p>6.93</p>
                  <p>0.0952</p>
                  <p>0.0329</p>
                  <p>0.0174</p>
                  <p>0.0118</p>
                  <p>0.00943</p>
                  <p>0.00844</p>
                  <p>0.00819</p>
                  <p>0.00846</p>
                  <p>11</p>
                  <p>7.62</p>
                  <p>0.0869</p>
                  <p>0.0276</p>
                  <p>0.0136</p>
                  <p>0.00864</p>
                  <p>0.0065</p>
                  <p>0.00552</p>
                  <p>0.00513</p>
                  <p>0.00509</p>
                  <p>12</p>
                  <p>8.32</p>
                  <p>0.08</p>
                  <p>0.0236</p>
                  <p>0.0108</p>
                  <p>0.00646</p>
                  <p>0.00459</p>
                  <p>0.00371</p>
                  <p>0.00329</p>
                  <p>0.00314</p>
                  <p>13</p>
                  <p>9.01</p>
                  <p>0.074</p>
                  <p>0.0203</p>
                  <p>0.00875</p>
                  <p>0.00492</p>
                  <p>0.00332</p>
                  <p>0.00255</p>
                  <p>0.00217</p>
                  <p>0.00199</p>
                  <p>14</p>
                  <p>9.7</p>
                  <p>0.0689</p>
                  <p>0.0177</p>
                  <p>0.00718</p>
                  <p>0.00381</p>
                  <p>0.00244</p>
                  <p>0.00179</p>
                  <p>0.00146</p>
                  <p>0.00129</p>
                  <p>15</p>
                  <p>10.4</p>
                  <p>0.0645</p>
                  <p>0.0156</p>
                  <p>0.00596</p>
                  <p>0.003</p>
                  <p>0.00183</p>
                  <p>0.00128</p>
                  <p>0.001</p>
                  <p>0.000852</p>
                  <p>16</p>
                  <p>11.1</p>
                  <p>0.0606</p>
                  <p>0.0138</p>
                  <p>0.005</p>
                  <p>0.00239</p>
                  <p>0.00139</p>
                  <p>0.000935</p>
                  <p>0.000702</p>
                  <p>0.000574</p>
                  <p>17</p>
                  <p>11.8</p>
                  <p>0.0571</p>
                  <p>0.0123</p>
                  <p>0.00423</p>
                  <p>0.00193</p>
                  <p>0.00107</p>
                  <p>0.000692</p>
                  <p>0.000499</p>
                  <p>0.000394</p>
                  <p>18</p>
                  <p>12.5</p>
                  <p>0.054</p>
                  <p>0.0111</p>
                  <p>0.00362</p>
                  <p>0.00158</p>
                  <p>0.000839</p>
                  <p>0.000519</p>
                  <p>0.00036</p>
                  <p>0.000275</p>
                  <p>19</p>
                  <p>13.2</p>
                  <p>0.0513</p>
                  <p>0.00998</p>
                  <p>0.00312</p>
                  <p>0.0013</p>
                  <p>0.000663</p>
                  <p>0.000394</p>
                  <p>0.000264</p>
                  <p>0.000194</p>
                  <p>20</p>
                  <p>13.9</p>
                  <p>0.0488</p>
                  <p>0.00906</p>
                  <p>0.0027</p>
                  <p>0.00108</p>
                  <p>0.00053</p>
                  <p>0.000303</p>
                  <p>0.000196</p>
                  <p>0.00014</p>
                  <p>21</p>
                  <p>14.6</p>
                  <p>0.0465</p>
                  <p>0.00825</p>
                  <p>0.00236</p>
                  <p>0.000905</p>
                  <p>0.000427</p>
                  <p>0.000236</p>
                  <p>0.000147</p>
                  <p>0.000101</p>
                  <p>22</p>
                  <p>15.2</p>
                  <p>0.0444</p>
                  <p>0.00755</p>
                  <p>0.00207</p>
                  <p>0.000764</p>
                  <p>0.000347</p>
                  <p>0.000185</p>
                  <p>0.000112</p>
                  <p>7.46e-05</p>
                  <p>23</p>
                  <p>15.9</p>
                  <p>0.0425</p>
                  <p>0.00694</p>
                  <p>0.00183</p>
                  <p>0.000649</p>
                  <p>0.000285</p>
                  <p>0.000147</p>
                  <p>8.56e-05</p>
                  <p>5.55e-05</p>
                  <p>24</p>
                  <p>16.6</p>
                  <p>0.0408</p>
                  <p>0.00639</p>
                  <p>0.00162</p>
                  <p>0.000555</p>
                  <p>0.000235</p>
                  <p>0.000117</p>
                  <p>6.63e-05</p>
                  <p>4.17e-05</p>
                  <p>25</p>
                  <p>17.3</p>
                  <p>0.0392</p>
                  <p>0.00591</p>
                  <p>0.00145</p>
                  <p>0.000478</p>
                  <p>0.000196</p>
                  <p>9.44e-05</p>
                  <p>5.18e-05</p>
                  <p>3.16e-05</p>
                  <p>26</p>
                  <p>18</p>
                  <p>0.0377</p>
                  <p>0.00548</p>
                  <p>0.00129</p>
                  <p>0.000413</p>
                  <p>0.000164</p>
                  <p>7.66e-05</p>
                  <p>4.08e-05</p>
                  <p>2.42e-05</p>
                  <p>27</p>
                  <p>18.7</p>
                  <p>0.0364</p>
                  <p>0.0051</p>
                  <p>0.00116</p>
                  <p>0.000359</p>
                  <p>0.000138</p>
                  <p>6.26e-05</p>
                  <p>3.24e-05</p>
                  <p>1.87e-05</p>
                  <p>28</p>
                  <p>19.4</p>
                  <p>0.0351</p>
                  <p>0.00475</p>
                  <p>0.00105</p>
                  <p>0.000314</p>
                  <p>0.000117</p>
                  <p>5.15e-05</p>
                  <p>2.59e-05</p>
                  <p>1.46e-05</p>
                  <p>29</p>
                  <p>20.1</p>
                  <p>0.0339</p>
                  <p>0.00444</p>
                  <p>0.000949</p>
                  <p>0.000276</p>
                  <p>9.96e-05</p>
                  <p>4.26e-05</p>
                  <p>2.09e-05</p>
                  <p>1.14e-05</p>
                  <p>30</p>
                  <p>20.8</p>
                  <p>0.0328</p>
                  <p>0.00416</p>
                  <p>0.000862</p>
                  <p>0.000243</p>
                  <p>8.53e-05</p>
                  <p>3.55e-05</p>
                  <p>1.69e-05</p>
                  <p>9.01e-06</p>
                  <p>31</p>
                  <p>21.5</p>
                  <p>0.0317</p>
                  <p>0.0039</p>
                  <p>0.000785</p>
                  <p>0.000215</p>
                  <p>7.33e-05</p>
                  <p>2.97e-05</p>
                  <p>1.38e-05</p>
                  <p>7.16e-06</p>
                  <p>32</p>
                  <p>22.2</p>
                  <p>0.0308</p>
                  <p>0.00367</p>
                  <p>0.000717</p>
                  <p>0.000191</p>
                  <p>6.33e-05</p>
                  <p>2.5e-05</p>
                  <p>1.13e-05</p>
                  <p>5.73e-06</p>
                  <p>表 14-1 中第一列为 m/n 的值，第二列为最优 k 值，其后列为不同 k 值的误判概率，可以看到当 k 值确定时，随着 m/n 的增大，误判概率逐渐变小。当 m/n 的值确定时，当 k 越靠近最优 K 值，误判概率越小。另外误判概率总体来看都是极小的，在容忍此误判概率的情况下，大幅减小存储空间和判定速度是完全值得的。 接下来我们就将 BloomFilter 算法应用到 Scrapy-Redis 分布式爬虫的去重过程中，以解决 Redis 内存不足的问题。</p>
                  <h3 id="3-对接-Scrapy-Redis"><a href="#3-对接-Scrapy-Redis" class="headerlink" title="3. 对接 Scrapy-Redis"></a>3. 对接 Scrapy-Redis</h3>
                  <p>实现 BloomFilter 时，我们首先要保证不能破坏 Scrapy-Redis 分布式爬取的运行架构，所以我们需要修改 Scrapy-Redis 的源码，将它的去重类替换掉。同时 BloomFilter 的实现需要借助于一个位数组，所以既然当前架构还是依赖于 Redis 的，那么正好位数组的维护直接使用 Redis 就好了。 首先我们实现一个基本的哈希算法，可以实现将一个值经过哈希运算后映射到一个 m 位位数组的某一位上，代码实现如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">HashMap</span><span class="params">(object)</span>:</span></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">__init__</span><span class="params">(self, m, seed)</span>:</span></span><br><span class="line">        self.m = m</span><br><span class="line">        self.seed = seed</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">hash</span><span class="params">(self, value)</span>:</span></span><br><span class="line">        <span class="string">"""</span></span><br><span class="line"><span class="string">        Hash Algorithm</span></span><br><span class="line"><span class="string">        :param value: Value</span></span><br><span class="line"><span class="string">        :return: Hash Value</span></span><br><span class="line"><span class="string">        """</span></span><br><span class="line">        ret = <span class="number">0</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> range(len(value)):</span><br><span class="line">            ret += self.seed * ret + ord(value[i])</span><br><span class="line">        <span class="keyword">return</span> (self.m - <span class="number">1</span>) &amp; ret</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里新建了一个 HashMap 类，构造函数传入两个值，一个是 m 位数组的位数，另一个是种子值 seed，不同的哈希函数需要有不同的 seed，这样可以保证不同的哈希函数的结果不会碰撞。 在 hash() 方法的实现中，value 是要被处理的内容，在这里我们遍历了该字符的每一位并利用 ord() 方法取到了它的 ASCII 码值，然后混淆 seed 进行迭代求和运算，最终会得到一个数值。这个数值的结果就由 value 和 seed 唯一确定，然后我们再将它和 m 进行按位与运算，即可获取到 m 位数组的映射结果，这样我们就实现了一个由字符串和 seed 来确定的哈希函数。当 m 固定时，只要 seed 值相同，就代表是同一个哈希函数，相同的 value 必然会映射到相同的位置。所以如果我们想要构造几个不同的哈希函数，只需要改变其 seed 就好了，以上便是一个简易的哈希函数的实现。 接下来我们再实现 BloomFilter，BloomFilter 里面需要用到 k 个哈希函数，所以在这里我们需要对这几个哈希函数指定相同的 m 值和不同的 seed 值，在这里构造如下：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">BLOOMFILTER_HASH_NUMBER = 6</span><br><span class="line">BLOOMFILTER_BIT = 30</span><br><span class="line"></span><br><span class="line">class BloomFilter(object):</span><br><span class="line">    def __init__(self, server, key, <span class="attribute">bit</span>=BLOOMFILTER_BIT, <span class="attribute">hash_number</span>=BLOOMFILTER_HASH_NUMBER):</span><br><span class="line">        <span class="string">""</span><span class="string">"</span></span><br><span class="line"><span class="string">        Initialize BloomFilter</span></span><br><span class="line"><span class="string">        :param server: Redis Server</span></span><br><span class="line"><span class="string">        :param key: BloomFilter Key</span></span><br><span class="line"><span class="string">        :param bit: m = 2 ^ bit</span></span><br><span class="line"><span class="string">        :param hash_number: the number of hash function</span></span><br><span class="line"><span class="string">        "</span><span class="string">""</span></span><br><span class="line">        #<span class="built_in"> default </span><span class="keyword">to</span> 1 &lt;&lt; 30 = 10,7374,1824 = 2^30 = 128MB, max<span class="built_in"> filter </span>2^30/hash_number = 1,7895,6970 fingerprints</span><br><span class="line">        self.m = 1 &lt;&lt; bit</span><br><span class="line">        self.seeds = range(hash_number)</span><br><span class="line">        self.maps = [HashMap(self.m, seed) <span class="keyword">for</span> seed <span class="keyword">in</span> self.seeds]</span><br><span class="line">        self.server = server</span><br><span class="line">        self.key = key</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>由于我们需要亿级别的数据的去重，即前文介绍的算法中的 n 为 1 亿以上，哈希函数的个数 k 大约取 10 左右的量级，而 m&gt;kn，所以这里 m 值大约保底在 10 亿，由于这个数值比较大，所以这里用移位操作来实现，传入位数 bit，定义 30，然后做一个移位操作 1 &lt;&lt; 30，相当于 2 的 30 次方，等于 1073741824，量级也是恰好在 10 亿左右，由于是位数组，所以这个位数组占用的大小就是 2^30b=128MB，而本文开头我们计算过 Scrapy-Redis 集合去重的占用空间大约在 2G 左右，可见 BloomFilter 的空间利用效率之高。 随后我们再传入哈希函数的个数，用它来生成几个不同的 seed，用不同的 seed 来定义不同的哈希函数，这样我们就可以构造一个哈希函数列表，遍历 seed，构造带有不同 seed 值的 HashMap 对象，保存成变量 maps 供后续使用。 另外 server 就是 Redis 连接对象，key 就是这个 m 位数组的名称。 接下来我们就要实现比较关键的两个方法了，一个是判定元素是否重复的方法 exists()，另一个是添加元素到集合中的方法 insert()，实现如下：</p>
                  <figure class="highlight python">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">exists</span><span class="params">(self, value)</span>:</span></span><br><span class="line">    <span class="string">"""</span></span><br><span class="line"><span class="string">    if value exists</span></span><br><span class="line"><span class="string">    :param value:</span></span><br><span class="line"><span class="string">    :return:</span></span><br><span class="line"><span class="string">    """</span></span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">not</span> value:</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">False</span></span><br><span class="line">    exist = <span class="number">1</span></span><br><span class="line">    <span class="keyword">for</span> map <span class="keyword">in</span> self.maps:</span><br><span class="line">        offset = map.hash(value)</span><br><span class="line">        exist = exist &amp; self.server.getbit(self.key, offset)</span><br><span class="line">    <span class="keyword">return</span> exist</span><br><span class="line"></span><br><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">insert</span><span class="params">(self, value)</span>:</span></span><br><span class="line">    <span class="string">"""</span></span><br><span class="line"><span class="string">    add value to bloom</span></span><br><span class="line"><span class="string">    :param value:</span></span><br><span class="line"><span class="string">    :return:</span></span><br><span class="line"><span class="string">    """</span></span><br><span class="line">    <span class="keyword">for</span> f <span class="keyword">in</span> self.maps:</span><br><span class="line">        offset = f.hash(value)</span><br><span class="line">        self.server.setbit(self.key, offset, <span class="number">1</span>)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>首先我们先看下 insert() 方法，BloomFilter 算法中会逐个调用哈希函数对放入集合中的元素进行运算得到在 m 位位数组中的映射位置，然后将位数组对应的位置置 1，所以这里在代码中我们遍历了初始化好的哈希函数，然后调用其 hash() 方法算出映射位置 offset，再利用 Redis 的 setbit() 方法将该位置 1。 在 exists() 方法中我们就需要实现判定是否重复的逻辑了，方法参数 value 即为待判断的元素，在这里我们首先定义了一个变量 exist，然后遍历了所有哈希函数对 value 进行哈希运算，得到映射位置，然后我们用 getbit() 方法取得该映射位置的结果，依次进行与运算。这样只有每次 getbit() 得到的结果都为 1 时，最后的 exist 才为 True，即代表 value 属于这个集合。如果其中只要有一次 getbit() 得到的结果为 0，即 m 位数组中有对应的 0 位，那么最终的结果 exist 就为 False，即代表 value 不属于这个集合。这样此方法最后的返回结果就是判定重复与否的结果了。 到现在为止 BloomFilter 的实现就已经完成了，我们可以用一个实例来测试一下，代码如下：</p>
                  <figure class="highlight vim">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">conn = StrictRedis(host=<span class="string">'localhost'</span>, port=<span class="number">6379</span>, password=<span class="string">'foobared'</span>)</span><br><span class="line"><span class="keyword">bf</span> = BloomFilter(conn, <span class="string">'testbf'</span>, <span class="number">5</span>, <span class="number">6</span>)</span><br><span class="line"><span class="keyword">bf</span>.<span class="keyword">insert</span>(<span class="string">'Hello'</span>)</span><br><span class="line"><span class="keyword">bf</span>.<span class="keyword">insert</span>(<span class="string">'World'</span>)</span><br><span class="line">result = <span class="keyword">bf</span>.<span class="built_in">exists</span>(<span class="string">'Hello'</span>)</span><br><span class="line"><span class="keyword">print</span>(bool(result))</span><br><span class="line">result = <span class="keyword">bf</span>.<span class="built_in">exists</span>(<span class="string">'Python'</span>)</span><br><span class="line"><span class="keyword">print</span>(bool(result))</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在这里我们首先定义了一个 Redis 连接对象，然后传递给 BloomFilter，为了避免内存占用过大这里传的位数 bit 比较小，设置为 5，哈希函数的个数设置为 6。 首先我们调用 insert() 方法插入了 Hello 和 World 两个字符串，随后判断了一下 Hello 和 Python 这两个字符串是否存在，最后输出它的结果，运行结果如下：</p>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="literal">True</span></span><br><span class="line"><span class="literal">False</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>很明显，结果完全没有问题，这样我们就借助于 Redis 成功实现了 BloomFilter 的算法。 接下来我们需要继续修改 Scrapy-Redis 的源码，将它的 dupefilter 逻辑替换为 BloomFilter 的逻辑，在这里主要是修改 RFPDupeFilter 类的 request_seen() 方法，实现如下：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="function"><span class="keyword">def</span> <span class="title">request_seen</span><span class="params">(<span class="keyword">self</span>, request)</span></span><span class="symbol">:</span></span><br><span class="line">    fp = <span class="keyword">self</span>.request_fingerprint(request)</span><br><span class="line">    <span class="keyword">if</span> <span class="keyword">self</span>.bf.exists(fp)<span class="symbol">:</span></span><br><span class="line">        <span class="keyword">return</span> True</span><br><span class="line">    <span class="keyword">self</span>.bf.insert(fp)</span><br><span class="line">    <span class="keyword">return</span> False</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>首先还是利用 request_fingerprint() 方法获取了 Request 的指纹，然后调用 BloomFilter 的 exists() 方法判定了该指纹是否存在，如果存在，则证明该 Request 是重复的，返回 True，否则调用 BloomFilter 的 insert() 方法将该指纹添加并返回 False，这样就成功利用 BloomFilter 替换了 Scrapy-Redis 的集合去重。 对于 BloomFilter 的初始化定义，我们可以将 <strong>init</strong>() 方法修改为如下内容：</p>
                  <figure class="highlight routeros">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">def __init__(self, server, key, debug, bit, hash_number):</span><br><span class="line">    self.server = server</span><br><span class="line">    self.key = key</span><br><span class="line">    self.<span class="builtin-name">debug</span> = debug</span><br><span class="line">    self.bit = bit</span><br><span class="line">    self.hash_number = hash_number</span><br><span class="line">    self.logdupes = <span class="literal">True</span></span><br><span class="line">    self.bf = BloomFilter(server, self.key, bit, hash_number)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>其中 bit 和 hash_number 需要使用 from_settings() 方法传递，修改如下：</p>
                  <figure class="highlight pgsql">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">@classmethod</span><br><span class="line">def from_settings(cls, settings):</span><br><span class="line">    <span class="keyword">server</span> = get_redis_from_settings(settings)</span><br><span class="line">    key = defaults.DUPEFILTER_KEY % &#123;<span class="string">'timestamp'</span>: <span class="type">int</span>(<span class="type">time</span>.time())&#125;</span><br><span class="line">    <span class="keyword">debug</span> = settings.getbool(<span class="string">'DUPEFILTER_DEBUG'</span>, DUPEFILTER_DEBUG)</span><br><span class="line">    <span class="type">bit</span> = settings.getint(<span class="string">'BLOOMFILTER_BIT'</span>, BLOOMFILTER_BIT)</span><br><span class="line">    hash_number = settings.getint(<span class="string">'BLOOMFILTER_HASH_NUMBER'</span>, BLOOMFILTER_HASH_NUMBER)</span><br><span class="line">    <span class="keyword">return</span> cls(<span class="keyword">server</span>, key=key, <span class="keyword">debug</span>=<span class="keyword">debug</span>, <span class="type">bit</span>=<span class="type">bit</span>, hash_number=hash_number)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>其中常量的定义 DUPEFILTER_DEBUG 和 BLOOMFILTER_BIT 统一定义在 defaults.py 中，默认如下：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="attr">BLOOMFILTER_HASH_NUMBER</span> = <span class="number">6</span></span><br><span class="line"><span class="attr">BLOOMFILTER_BIT</span> = <span class="number">30</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>到此为止我们就成功实现了 BloomFilter 和 Scrapy-Redis 的对接。</p>
                  <h3 id="4-本节代码"><a href="#4-本节代码" class="headerlink" title="4. 本节代码"></a>4. 本节代码</h3>
                  <p>本节代码地址为：<a href="https://github.com/Python3WebSpider/ScrapyRedisBloomFilter" target="_blank" rel="noopener">https://github.com/Python3WebSpider/ScrapyRedisBloomFilter</a>。</p>
                  <h3 id="5-使用"><a href="#5-使用" class="headerlink" title="5. 使用"></a>5. 使用</h3>
                  <p>为了方便使用，本节的代码已经打包成了一个 Python 包并发布到了 PyPi，链接为：<a href="https://pypi.python.org/pypi/scrapy-redis-bloomfilter" target="_blank" rel="noopener">https://pypi.python.org/pypi/scrapy-redis-bloomfilter</a>，因此我们以后如果想使用 ScrapyRedisBloomFilter 直接使用就好了，不需要再自己实现一遍。 我们可以直接使用 Pip 来安装，命令如下：</p>
                  <figure class="highlight cmake">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">pip3 <span class="keyword">install</span> scrapy-redis-bloomfilter</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>使用的方法和 Scrapy-Redis 基本相似，在这里说明几个关键配置：</p>
                  <figure class="highlight ini">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="comment"># 去重类，要使用 BloomFilter 请替换 DUPEFILTER_CLASS</span></span><br><span class="line"><span class="attr">DUPEFILTER_CLASS</span> = <span class="string">"scrapy_redis_bloomfilter.dupefilter.RFPDupeFilter"</span></span><br><span class="line"><span class="comment"># 哈希函数的个数，默认为 6，可以自行修改</span></span><br><span class="line"><span class="attr">BLOOMFILTER_HASH_NUMBER</span> = <span class="number">6</span></span><br><span class="line"><span class="comment"># BloomFilter 的 bit 参数，默认 30，占用 128MB 空间，去重量级 1 亿</span></span><br><span class="line"><span class="attr">BLOOMFILTER_BIT</span> = <span class="number">30</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>DUPEFILTER_CLASS 是去重类，如果要使用 BloomFilter 需要将 DUPEFILTER_CLASS 修改为该包的去重类。 BLOOMFILTER_HASH_NUMBER 是 BloomFilter 使用的哈希函数的个数，默认为 6，可以根据去重量级自行修改。 BLOOMFILTER_BIT 即前文所介绍的 BloomFilter 类的 bit 参数，它决定了位数组的位数，如果 BLOOMFILTER_BIT 为 30，那么位数组位数为 2 的 30 次方，将占用 Redis 128MB 的存储空间，去重量级在 1 亿左右，即对应爬取量级 1 亿左右。如果爬取量级在 10 亿、20 亿甚至 100 亿，请务必将此参数对应调高。</p>
                  <h3 id="6-测试"><a href="#6-测试" class="headerlink" title="6. 测试"></a>6. 测试</h3>
                  <p>在源代码中附有一个测试项目，放在 tests 文件夹，该项目使用了 Scrapy-RedisBloomFilter 来去重，Spider 的实现如下：</p>
                  <figure class="highlight ruby">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">from scrapy import Request, Spider</span><br><span class="line"></span><br><span class="line"><span class="class"><span class="keyword">class</span> <span class="title">TestSpider</span>(<span class="title">Spider</span>):</span></span><br><span class="line">    name = <span class="string">'test'</span></span><br><span class="line">    base_url = <span class="string">'https://www.baidu.com/s?wd='</span></span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">start_requests</span><span class="params">(<span class="keyword">self</span>)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> range(<span class="number">10</span>)<span class="symbol">:</span></span><br><span class="line">            url = <span class="keyword">self</span>.base_url + str(i)</span><br><span class="line">            <span class="keyword">yield</span> Request(url, callback=<span class="keyword">self</span>.parse)</span><br><span class="line"></span><br><span class="line">        <span class="comment"># Here contains 10 duplicated Requests    </span></span><br><span class="line">        <span class="keyword">for</span> i <span class="keyword">in</span> range(<span class="number">100</span>): </span><br><span class="line">            url = <span class="keyword">self</span>.base_url + str(i)</span><br><span class="line">            <span class="keyword">yield</span> Request(url, callback=<span class="keyword">self</span>.parse)</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="keyword">def</span> <span class="title">parse</span><span class="params">(<span class="keyword">self</span>, response)</span></span><span class="symbol">:</span></span><br><span class="line">        <span class="keyword">self</span>.logger.debug(<span class="string">'Response of '</span> + response.url)</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>在 start_requests() 方法中首先循环 10 次，构造参数为 0-9 的 URL，然后重新循环了 100 次，构造了参数为 0-99 的 URL，那么这里就会包含 10 个重复的 Request，我们运行项目测试一下：</p>
                  <figure class="highlight bash">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line">scrapy crawl <span class="built_in">test</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以看到最后的输出结果如下：</p>
                  <figure class="highlight yaml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="string">&#123;'bloomfilter/filtered':</span> <span class="number">10</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'downloader/request_bytes':</span> <span class="number">34021</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'downloader/request_count':</span> <span class="number">100</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'downloader/request_method_count/GET':</span> <span class="number">100</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'downloader/response_bytes':</span> <span class="number">72943</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'downloader/response_count':</span> <span class="number">100</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'downloader/response_status_count/200':</span> <span class="number">100</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'finish_reason':</span> <span class="string">'finished'</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'finish_time':</span> <span class="string">datetime.datetime(2017,</span> <span class="number">8</span><span class="string">,</span> <span class="number">11</span><span class="string">,</span> <span class="number">9</span><span class="string">,</span> <span class="number">34</span><span class="string">,</span> <span class="number">30</span><span class="string">,</span> <span class="number">419597</span><span class="string">),</span></span><br><span class="line"> <span class="attr">'log_count/DEBUG':</span> <span class="number">202</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'log_count/INFO':</span> <span class="number">7</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'memusage/max':</span> <span class="number">54153216</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'memusage/startup':</span> <span class="number">54153216</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'response_received_count':</span> <span class="number">100</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'scheduler/dequeued/redis':</span> <span class="number">100</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'scheduler/enqueued/redis':</span> <span class="number">100</span><span class="string">,</span></span><br><span class="line"> <span class="attr">'start_time':</span> <span class="string">datetime.datetime(2017,</span> <span class="number">8</span><span class="string">,</span> <span class="number">11</span><span class="string">,</span> <span class="number">9</span><span class="string">,</span> <span class="number">34</span><span class="string">,</span> <span class="number">26</span><span class="string">,</span> <span class="number">495018</span><span class="string">)&#125;</span></span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>可以看到最后统计的第一行的结果：</p>
                  <figure class="highlight sml">
                    <table>
                      <tr>
                        <td class="gutter">
                          <pre><span class="line">1</span><br></pre>
                        </td>
                        <td class="code">
                          <pre><span class="line"><span class="symbol">'bloomfilter</span>/filtered': <span class="number">10</span>,</span><br></pre>
                        </td>
                      </tr>
                    </table>
                  </figure>
                  <p>这就是 BloomFilter 过滤后的统计结果，可以看到它的过滤个数为 10 个，也就是它成功将重复的 10 个 Reqeust 识别出来了，测试通过。</p>
                  <h3 id="7-结语"><a href="#7-结语" class="headerlink" title="7. 结语"></a>7. 结语</h3>
                  <p>以上便是 BloomFilter 的原理及对接实现，使用了 BloomFilter 可以大大节省 Redis 内存，在数据量大的情况下推荐使用此方案。</p>
                  </p>
                </div>
              </div>
              <div class="post-meta">
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-user"></i>
                  </span>
                  <span class="post-meta-item-text">作者</span>
                  <span><a href="/authors/崔庆才" class="author" itemprop="url" rel="index">崔庆才</a></span>
                </span>
                <span class="post-meta-item">
                  <span class="post-meta-item-icon">
                    <i class="far fa-calendar"></i>
                  </span>
                  <span class="post-meta-item-text">发表于</span>
                  <time title="创建时间：2019-12-10 09:24:45" itemprop="dateCreated datePublished" datetime="2019-12-10T09:24:45+08:00">2019-12-10</time>
                </span>
                <span id="/8472.html" class="post-meta-item leancloud_visitors" data-flag-title="[Python3网络爬虫开发实战] 14.4–Bloom Filter 的对接" title="阅读次数">
                  <span class="post-meta-item-icon">
                    <i class="fa fa-eye"></i>
                  </span>
                  <span class="post-meta-item-text">阅读次数：</span>
                  <span class="leancloud-visitors-count"></span>
                </span>
                <span class="post-meta-item" title="本文字数">
                  <span class="post-meta-item-icon">
                    <i class="far fa-file-word"></i>
                  </span>
                  <span class="post-meta-item-text">本文字数：</span>
                  <span>10k</span>
                </span>
                <span class="post-meta-item" title="阅读时长">
                  <span class="post-meta-item-icon">
                    <i class="far fa-clock"></i>
                  </span>
                  <span class="post-meta-item-text">阅读时长 &asymp;</span>
                  <span>9 分钟</span>
                </span>
              </div>
            </article>
            <script>
              document.querySelectorAll('.random').forEach(item => item.src = "https://picsum.photos/id/" + Math.floor(Math.random() * Math.floor(300)) + "/200/133")

            </script>
            <nav class="pagination">
              <a class="extend prev" rel="prev" href="/page/8/"><i class="fa fa-angle-left" aria-label="上一页"></i></a><a class="page-number" href="/">1</a><span class="space">&hellip;</span><a class="page-number" href="/page/8/">8</a><span class="page-number current">9</span><a class="page-number" href="/page/10/">10</a><span class="space">&hellip;</span><a class="page-number" href="/page/31/">31</a><a class="extend next" rel="next" href="/page/10/"><i class="fa fa-angle-right" aria-label="下一页"></i></a>
            </nav>
          </div>
          <script>
            window.addEventListener('tabs:register', () =>
            {
              let
              {
                activeClass
              } = CONFIG.comments;
              if (CONFIG.comments.storage)
              {
                activeClass = localStorage.getItem('comments_active') || activeClass;
              }
              if (activeClass)
              {
                let activeTab = document.querySelector(`a[href="#comment-${activeClass}"]`);
                if (activeTab)
                {
                  activeTab.click();
                }
              }
            });
            if (CONFIG.comments.storage)
            {
              window.addEventListener('tabs:click', event =>
              {
                if (!event.target.matches('.tabs-comment .tab-content .tab-pane')) return;
                let commentClass = event.target.classList[1];
                localStorage.setItem('comments_active', commentClass);
              });
            }

          </script>
        </div>
        <div class="toggle sidebar-toggle">
          <span class="toggle-line toggle-line-first"></span>
          <span class="toggle-line toggle-line-middle"></span>
          <span class="toggle-line toggle-line-last"></span>
        </div>
        <aside class="sidebar">
          <div class="sidebar-inner">
            <ul class="sidebar-nav motion-element">
              <li class="sidebar-nav-toc"> 文章目录 </li>
              <li class="sidebar-nav-overview"> 站点概览 </li>
            </ul>
            <!--noindex-->
            <div class="post-toc-wrap sidebar-panel">
            </div>
            <!--/noindex-->
            <div class="site-overview-wrap sidebar-panel">
              <div class="site-author motion-element" itemprop="author" itemscope itemtype="http://schema.org/Person">
                <img class="site-author-image" itemprop="image" alt="崔庆才" src="/images/avatar.png">
                <p class="site-author-name" itemprop="name">崔庆才</p>
                <div class="site-description" itemprop="description">崔庆才的个人站点，记录生活的瞬间，分享学习的心得。</div>
              </div>
              <div class="site-state-wrap motion-element">
                <nav class="site-state">
                  <div class="site-state-item site-state-posts">
                    <a href="/archives/">
                      <span class="site-state-item-count">608</span>
                      <span class="site-state-item-name">日志</span>
                    </a>
                  </div>
                  <div class="site-state-item site-state-categories">
                    <a href="/categories/">
                      <span class="site-state-item-count">24</span>
                      <span class="site-state-item-name">分类</span></a>
                  </div>
                  <div class="site-state-item site-state-tags">
                    <a href="/tags/">
                      <span class="site-state-item-count">156</span>
                      <span class="site-state-item-name">标签</span></a>
                  </div>
                </nav>
              </div>
              <div class="links-of-author motion-element">
                <span class="links-of-author-item">
                  <a href="https://github.com/Germey" title="GitHub → https:&#x2F;&#x2F;github.com&#x2F;Germey" rel="noopener" target="_blank"><i class="fab fa-github fa-fw"></i>GitHub</a>
                </span>
                <span class="links-of-author-item">
                  <a href="mailto:cqc@cuiqingcai.com.com" title="邮件 → mailto:cqc@cuiqingcai.com.com" rel="noopener" target="_blank"><i class="fa fa-envelope fa-fw"></i>邮件</a>
                </span>
                <span class="links-of-author-item">
                  <a href="https://weibo.com/cuiqingcai" title="微博 → https:&#x2F;&#x2F;weibo.com&#x2F;cuiqingcai" rel="noopener" target="_blank"><i class="fab fa-weibo fa-fw"></i>微博</a>
                </span>
                <span class="links-of-author-item">
                  <a href="https://www.zhihu.com/people/Germey" title="知乎 → https:&#x2F;&#x2F;www.zhihu.com&#x2F;people&#x2F;Germey" rel="noopener" target="_blank"><i class="fa fa-magic fa-fw"></i>知乎</a>
                </span>
              </div>
            </div>
            <div style=" width: 100%;" class="sidebar-panel sidebar-panel-image sidebar-panel-active">
              <a href="https://tutorial.lengyue.video/?coupon=12ef4b1a-a3db-11ea-bb37-0242ac130002_cqx_850" target="_blank" rel="noopener">
                <img src="https://qiniu.cuiqingcai.com/bco2a.png" style=" width: 100%;">
              </a>
            </div>
            <div style=" width: 100%;" class="sidebar-panel sidebar-panel-image sidebar-panel-active">
              <a href="http://www.ipidea.net/?utm-source=cqc&utm-keyword=?cqc" target="_blank" rel="noopener">
                <img src="https://qiniu.cuiqingcai.com/0ywun.png" style=" width: 100%;">
              </a>
            </div>
            <div class="sidebar-panel sidebar-panel-tags sidebar-panel-active">
              <h4 class="name"> 标签云 </h4>
              <div class="content">
                <a href="/tags/2048/" style="font-size: 10px;">2048</a> <a href="/tags/API/" style="font-size: 10px;">API</a> <a href="/tags/Bootstrap/" style="font-size: 11.25px;">Bootstrap</a> <a href="/tags/CDN/" style="font-size: 10px;">CDN</a> <a href="/tags/CQC/" style="font-size: 10px;">CQC</a> <a href="/tags/CSS/" style="font-size: 10px;">CSS</a> <a href="/tags/CSS-%E5%8F%8D%E7%88%AC%E8%99%AB/" style="font-size: 10px;">CSS 反爬虫</a> <a href="/tags/CV/" style="font-size: 10px;">CV</a> <a href="/tags/Django/" style="font-size: 10px;">Django</a> <a href="/tags/Eclipse/" style="font-size: 11.25px;">Eclipse</a> <a href="/tags/FTP/" style="font-size: 10px;">FTP</a> <a href="/tags/Git/" style="font-size: 10px;">Git</a> <a href="/tags/GitHub/" style="font-size: 13.75px;">GitHub</a> <a href="/tags/HTML5/" style="font-size: 10px;">HTML5</a> <a href="/tags/Hexo/" style="font-size: 10px;">Hexo</a> <a href="/tags/IT/" style="font-size: 10px;">IT</a> <a href="/tags/JSP/" style="font-size: 10px;">JSP</a> <a href="/tags/JavaScript/" style="font-size: 10px;">JavaScript</a> <a href="/tags/K8s/" style="font-size: 10px;">K8s</a> <a href="/tags/LOGO/" style="font-size: 10px;">LOGO</a> <a href="/tags/Linux/" style="font-size: 10px;">Linux</a> <a href="/tags/MIUI/" style="font-size: 10px;">MIUI</a> <a href="/tags/MongoDB/" style="font-size: 10px;">MongoDB</a> <a href="/tags/Mysql/" style="font-size: 10px;">Mysql</a> <a href="/tags/NBA/" style="font-size: 10px;">NBA</a> <a href="/tags/PHP/" style="font-size: 11.25px;">PHP</a> <a href="/tags/PS/" style="font-size: 10px;">PS</a> <a href="/tags/Pathlib/" style="font-size: 10px;">Pathlib</a> <a href="/tags/PhantomJS/" style="font-size: 10px;">PhantomJS</a> <a href="/tags/Python/" style="font-size: 15px;">Python</a> <a href="/tags/Python3/" style="font-size: 12.5px;">Python3</a> <a href="/tags/Pythonic/" style="font-size: 10px;">Pythonic</a> <a href="/tags/QQ/" style="font-size: 10px;">QQ</a> <a href="/tags/Redis/" style="font-size: 10px;">Redis</a> <a href="/tags/SAE/" style="font-size: 10px;">SAE</a> <a href="/tags/SSH/" style="font-size: 10px;">SSH</a> <a href="/tags/SVG/" style="font-size: 10px;">SVG</a> <a href="/tags/Scrapy/" style="font-size: 10px;">Scrapy</a> <a href="/tags/Scrapy-redis/" style="font-size: 10px;">Scrapy-redis</a> <a href="/tags/Scrapy%E5%88%86%E5%B8%83%E5%BC%8F/" style="font-size: 10px;">Scrapy分布式</a> <a href="/tags/Selenium/" style="font-size: 10px;">Selenium</a> <a href="/tags/TKE/" style="font-size: 10px;">TKE</a> <a href="/tags/Ubuntu/" style="font-size: 11.25px;">Ubuntu</a> <a href="/tags/VS-Code/" style="font-size: 10px;">VS Code</a> <a href="/tags/Vs-Code/" style="font-size: 10px;">Vs Code</a> <a href="/tags/Vue/" style="font-size: 11.25px;">Vue</a> <a href="/tags/Webpack/" style="font-size: 10px;">Webpack</a> <a href="/tags/Windows/" style="font-size: 10px;">Windows</a> <a href="/tags/Winpcap/" style="font-size: 10px;">Winpcap</a> <a href="/tags/WordPress/" style="font-size: 13.75px;">WordPress</a> <a href="/tags/Youtube/" style="font-size: 11.25px;">Youtube</a> <a href="/tags/android/" style="font-size: 10px;">android</a> <a href="/tags/ansible/" style="font-size: 10px;">ansible</a> <a href="/tags/cocos2d-x/" style="font-size: 10px;">cocos2d-x</a> <a href="/tags/e6/" style="font-size: 10px;">e6</a> <a href="/tags/fitvids/" style="font-size: 10px;">fitvids</a> <a href="/tags/git/" style="font-size: 11.25px;">git</a> <a href="/tags/json/" style="font-size: 10px;">json</a> <a href="/tags/js%E9%80%86%E5%90%91/" style="font-size: 10px;">js逆向</a> <a href="/tags/kubernetes/" style="font-size: 10px;">kubernetes</a> <a href="/tags/log/" style="font-size: 10px;">log</a> <a href="/tags/logging/" style="font-size: 10px;">logging</a> <a href="/tags/matlab/" style="font-size: 11.25px;">matlab</a> <a href="/tags/python/" style="font-size: 20px;">python</a> <a href="/tags/pytube/" style="font-size: 11.25px;">pytube</a> <a href="/tags/pywin32/" style="font-size: 10px;">pywin32</a> <a href="/tags/style/" style="font-size: 10px;">style</a> <a href="/tags/tomcat/" style="font-size: 10px;">tomcat</a> <a href="/tags/ubuntu/" style="font-size: 10px;">ubuntu</a> <a href="/tags/uwsgi/" style="font-size: 10px;">uwsgi</a> <a href="/tags/vsftpd/" style="font-size: 10px;">vsftpd</a> <a href="/tags/wamp/" style="font-size: 10px;">wamp</a> <a href="/tags/wineQQ/" style="font-size: 10px;">wineQQ</a> <a href="/tags/%E4%B8%83%E7%89%9B/" style="font-size: 11.25px;">七牛</a> <a href="/tags/%E4%B8%8A%E6%B5%B7/" style="font-size: 10px;">上海</a> <a href="/tags/%E4%B8%AA%E4%BA%BA%E7%BD%91%E7%AB%99/" style="font-size: 10px;">个人网站</a> <a href="/tags/%E4%B8%BB%E9%A2%98/" style="font-size: 10px;">主题</a> <a href="/tags/%E4%BA%91%E4%BA%A7%E5%93%81/" style="font-size: 10px;">云产品</a> <a href="/tags/%E4%BA%91%E5%AD%98%E5%82%A8/" style="font-size: 10px;">云存储</a> <a href="/tags/%E4%BA%AC%E4%B8%9C%E4%BA%91/" style="font-size: 10px;">京东云</a> <a href="/tags/%E4%BA%BA%E5%B7%A5%E6%99%BA%E8%83%BD/" style="font-size: 12.5px;">人工智能</a> <a href="/tags/%E4%BB%A3%E7%90%86/" style="font-size: 10px;">代理</a> <a href="/tags/%E4%BB%A3%E7%A0%81/" style="font-size: 10px;">代码</a> <a href="/tags/%E4%BB%A3%E7%A0%81%E5%88%86%E4%BA%AB%E5%9B%BE/" style="font-size: 10px;">代码分享图</a> <a href="/tags/%E4%BC%98%E5%8C%96/" style="font-size: 10px;">优化</a> <a href="/tags/%E4%BD%8D%E8%BF%90%E7%AE%97/" style="font-size: 10px;">位运算</a> <a href="/tags/%E5%85%AC%E4%BC%97%E5%8F%B7/" style="font-size: 10px;">公众号</a> <a href="/tags/%E5%88%86%E4%BA%AB/" style="font-size: 10px;">分享</a> <a href="/tags/%E5%88%86%E5%B8%83%E5%BC%8F/" style="font-size: 10px;">分布式</a> <a href="/tags/%E5%88%9B%E4%B8%9A/" style="font-size: 10px;">创业</a> <a href="/tags/%E5%89%8D%E7%AB%AF/" style="font-size: 12.5px;">前端</a> <a href="/tags/%E5%8D%9A%E5%AE%A2/" style="font-size: 10px;">博客</a> <a href="/tags/%E5%8E%9F%E7%94%9FAPP/" style="font-size: 10px;">原生APP</a> <a href="/tags/%E5%8F%8D%E7%88%AC%E8%99%AB/" style="font-size: 12.5px;">反爬虫</a> <a href="/tags/%E5%91%BD%E4%BB%A4/" style="font-size: 10px;">命令</a> <a href="/tags/%E5%93%8D%E5%BA%94%E5%BC%8F%E5%B8%83%E5%B1%80/" style="font-size: 10px;">响应式布局</a> <a href="/tags/%E5%9E%83%E5%9C%BE%E9%82%AE%E4%BB%B6/" style="font-size: 10px;">垃圾邮件</a> <a href="/tags/%E5%9F%9F%E5%90%8D%E7%BB%91%E5%AE%9A/" style="font-size: 10px;">域名绑定</a> <a href="/tags/%E5%A4%8D%E7%9B%98/" style="font-size: 10px;">复盘</a> <a href="/tags/%E5%A4%A7%E4%BC%97%E7%82%B9%E8%AF%84/" style="font-size: 10px;">大众点评</a> <a href="/tags/%E5%AD%97%E4%BD%93%E5%8F%8D%E7%88%AC%E8%99%AB/" style="font-size: 10px;">字体反爬虫</a> <a href="/tags/%E5%AD%97%E7%AC%A6%E9%97%AE%E9%A2%98/" style="font-size: 10px;">字符问题</a> <a href="/tags/%E5%AD%A6%E4%B9%A0%E6%96%B9%E6%B3%95/" style="font-size: 10px;">学习方法</a> <a href="/tags/%E5%AE%89%E5%8D%93/" style="font-size: 10px;">安卓</a> <a href="/tags/%E5%AE%9E%E7%94%A8/" style="font-size: 10px;">实用</a> <a href="/tags/%E5%B0%81%E9%9D%A2/" style="font-size: 10px;">封面</a> <a href="/tags/%E5%B4%94%E5%BA%86%E6%89%8D/" style="font-size: 18.75px;">崔庆才</a> <a href="/tags/%E5%B7%A5%E5%85%B7/" style="font-size: 12.5px;">工具</a> <a href="/tags/%E5%BC%80%E5%8F%91%E5%B7%A5%E5%85%B7/" style="font-size: 10px;">开发工具</a> <a href="/tags/%E5%BE%AE%E8%BD%AF/" style="font-size: 10px;">微软</a> <a href="/tags/%E6%80%9D%E8%80%83/" style="font-size: 10px;">思考</a> <a href="/tags/%E6%89%8B%E6%9C%BA%E8%AE%BF%E9%97%AE/" style="font-size: 10px;">手机访问</a> <a href="/tags/%E6%95%99%E7%A8%8B/" style="font-size: 10px;">教程</a> <a href="/tags/%E6%95%99%E8%82%B2/" style="font-size: 10px;">教育</a> <a href="/tags/%E6%96%B0%E4%B9%A6/" style="font-size: 12.5px;">新书</a> <a href="/tags/%E6%96%B9%E6%B3%95%E8%AE%BA/" style="font-size: 10px;">方法论</a> <a href="/tags/%E6%97%85%E6%B8%B8/" style="font-size: 10px;">旅游</a> <a href="/tags/%E6%97%A5%E5%BF%97/" style="font-size: 10px;">日志</a> <a href="/tags/%E6%9A%97%E6%97%B6%E9%97%B4/" style="font-size: 10px;">暗时间</a> <a href="/tags/%E6%9D%9C%E5%85%B0%E7%89%B9/" style="font-size: 11.25px;">杜兰特</a> <a href="/tags/%E6%A1%8C%E9%9D%A2/" style="font-size: 10px;">桌面</a> <a href="/tags/%E6%AD%8C%E5%8D%95/" style="font-size: 10px;">歌单</a> <a href="/tags/%E6%B1%9F%E5%8D%97/" style="font-size: 10px;">江南</a> <a href="/tags/%E6%B8%B8%E6%88%8F/" style="font-size: 10px;">游戏</a> <a href="/tags/%E7%84%A6%E8%99%91/" style="font-size: 10px;">焦虑</a> <a href="/tags/%E7%88%AC%E8%99%AB/" style="font-size: 16.25px;">爬虫</a> <a href="/tags/%E7%88%AC%E8%99%AB%E4%B9%A6%E7%B1%8D/" style="font-size: 11.25px;">爬虫书籍</a> <a href="/tags/%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F/" style="font-size: 10px;">环境变量</a> <a href="/tags/%E7%94%9F%E6%B4%BB%E7%AC%94%E8%AE%B0/" style="font-size: 10px;">生活笔记</a> <a href="/tags/%E7%99%BB%E5%BD%95/" style="font-size: 10px;">登录</a> <a href="/tags/%E7%9F%A5%E4%B9%8E/" style="font-size: 10px;">知乎</a> <a href="/tags/%E7%9F%AD%E4%BF%A1/" style="font-size: 10px;">短信</a> <a href="/tags/%E7%9F%AD%E4%BF%A1%E9%AA%8C%E8%AF%81%E7%A0%81/" style="font-size: 10px;">短信验证码</a> <a href="/tags/%E7%AC%94%E8%AE%B0%E8%BD%AF%E4%BB%B6/" style="font-size: 10px;">笔记软件</a> <a href="/tags/%E7%AF%AE%E7%BD%91/" style="font-size: 10px;">篮网</a> <a href="/tags/%E7%BA%B8%E5%BC%A0/" style="font-size: 10px;">纸张</a> <a href="/tags/%E7%BB%84%E4%BB%B6/" style="font-size: 10px;">组件</a> <a href="/tags/%E7%BD%91%E7%AB%99/" style="font-size: 10px;">网站</a> <a href="/tags/%E7%BD%91%E7%BB%9C%E7%88%AC%E8%99%AB/" style="font-size: 11.25px;">网络爬虫</a> <a href="/tags/%E7%BE%8E%E5%AD%A6/" style="font-size: 10px;">美学</a> <a href="/tags/%E8%82%89%E5%A4%B9%E9%A6%8D/" style="font-size: 10px;">肉夹馍</a> <a href="/tags/%E8%85%BE%E8%AE%AF%E4%BA%91/" style="font-size: 10px;">腾讯云</a> <a href="/tags/%E8%87%AA%E5%BE%8B/" style="font-size: 10px;">自律</a> <a href="/tags/%E8%A5%BF%E5%B0%91%E7%88%B7/" style="font-size: 10px;">西少爷</a> <a href="/tags/%E8%A7%86%E9%A2%91/" style="font-size: 10px;">视频</a> <a href="/tags/%E8%B0%B7%E6%AD%8C%E9%AA%8C%E8%AF%81%E7%A0%81/" style="font-size: 10px;">谷歌验证码</a> <a href="/tags/%E8%BF%90%E8%90%A5/" style="font-size: 10px;">运营</a> <a href="/tags/%E8%BF%9C%E7%A8%8B/" style="font-size: 10px;">远程</a> <a href="/tags/%E9%80%86%E5%90%91/" style="font-size: 10px;">逆向</a> <a href="/tags/%E9%85%8D%E7%BD%AE/" style="font-size: 10px;">配置</a> <a href="/tags/%E9%87%8D%E8%A3%85/" style="font-size: 10px;">重装</a> <a href="/tags/%E9%98%BF%E6%9D%9C/" style="font-size: 10px;">阿杜</a> <a href="/tags/%E9%9D%99%E8%A7%85/" style="font-size: 17.5px;">静觅</a> <a href="/tags/%E9%A2%A0%E8%A6%86/" style="font-size: 10px;">颠覆</a> <a href="/tags/%E9%A3%9E%E4%BF%A1/" style="font-size: 10px;">飞信</a> <a href="/tags/%E9%B8%BF%E8%92%99/" style="font-size: 10px;">鸿蒙</a>
              </div>
              <script>
                const tagsColors = ['#00a67c', '#5cb85c', '#d9534f', '#567e95', '#b37333', '#f4843d', '#15a287']
                const tagsElements = document.querySelectorAll('.sidebar-panel-tags .content a')
                tagsElements.forEach((item) =>
                {
                  item.style.backgroundColor = tagsColors[Math.floor(Math.random() * tagsColors.length)]
                })

              </script>
            </div>
            <div class="sidebar-panel sidebar-panel-categories sidebar-panel-active">
              <h4 class="name"> 分类 </h4>
              <div class="content">
                <ul class="category-list">
                  <li class="category-list-item"><a class="category-list-link" href="/categories/C-C/">C/C++</a><span class="category-list-count">23</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/HTML/">HTML</a><span class="category-list-count">14</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Java/">Java</a><span class="category-list-count">5</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/JavaScript/">JavaScript</a><span class="category-list-count">26</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Linux/">Linux</a><span class="category-list-count">15</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Markdown/">Markdown</a><span class="category-list-count">1</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Net/">Net</a><span class="category-list-count">4</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Other/">Other</a><span class="category-list-count">39</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/PHP/">PHP</a><span class="category-list-count">27</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Paper/">Paper</a><span class="category-list-count">2</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/Python/">Python</a><span class="category-list-count">261</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/TypeScript/">TypeScript</a><span class="category-list-count">2</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%AA%E4%BA%BA%E5%B1%95%E7%A4%BA/">个人展示</a><span class="category-list-count">1</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%AA%E4%BA%BA%E6%97%A5%E8%AE%B0/">个人日记</a><span class="category-list-count">9</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%AA%E4%BA%BA%E8%AE%B0%E5%BD%95/">个人记录</a><span class="category-list-count">4</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E4%B8%AA%E4%BA%BA%E9%9A%8F%E7%AC%94/">个人随笔</a><span class="category-list-count">15</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E5%AE%89%E8%A3%85%E9%85%8D%E7%BD%AE/">安装配置</a><span class="category-list-count">59</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E6%8A%80%E6%9C%AF%E6%9D%82%E8%B0%88/">技术杂谈</a><span class="category-list-count">88</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E6%9C%AA%E5%88%86%E7%B1%BB/">未分类</a><span class="category-list-count">1</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E7%94%9F%E6%B4%BB%E7%AC%94%E8%AE%B0/">生活笔记</a><span class="category-list-count">1</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E7%A6%8F%E5%88%A9%E4%B8%93%E5%8C%BA/">福利专区</a><span class="category-list-count">6</span></li>
                  <li class="category-list-item"><a class="category-list-link" href="/categories/%E8%81%8C%E4%BD%8D%E6%8E%A8%E8%8D%90/">职位推荐</a><span class="category-list-count">2</span></li>
                </ul>
              </div>
            </div>
            <div class="sidebar-panel sidebar-panel-friends sidebar-panel-active">
              <h4 class="name"> 友情链接 </h4>
              <ul class="friends">
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/j2dub.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.findhao.net/" target="_blank" rel="noopener">FindHao</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/ou6mm.jpg">
                  </span>
                  <span class="link">
                    <a href="https://diygod.me/" target="_blank" rel="noopener">DIYgod</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/6apxu.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.51dev.com/" target="_blank" rel="noopener">IT技术社区</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://www.jankl.com/img/titleshu.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.jankl.com/" target="_blank" rel="noopener">liberalist</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/bqlbs.png">
                  </span>
                  <span class="link">
                    <a href="http://www.urselect.com/" target="_blank" rel="noopener">优社电商</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/8s88c.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.yuanrenxue.com/" target="_blank" rel="noopener">猿人学</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/2wgg5.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.yunlifang.cn/" target="_blank" rel="noopener">云立方</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/shwr6.png">
                  </span>
                  <span class="link">
                    <a href="http://lanbing510.info/" target="_blank" rel="noopener">冰蓝</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/blvoh.jpg">
                  </span>
                  <span class="link">
                    <a href="https://lengyue.me/" target="_blank" rel="noopener">冷月</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="http://qianxunclub.com/favicon.png">
                  </span>
                  <span class="link">
                    <a href="http://qianxunclub.com/" target="_blank" rel="noopener">千寻啊千寻</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/0044u.jpg">
                  </span>
                  <span class="link">
                    <a href="http://kodcloud.com/" target="_blank" rel="noopener">可道云</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/ygnpn.jpg">
                  </span>
                  <span class="link">
                    <a href="http://www.kunkundashen.cn/" target="_blank" rel="noopener">坤坤大神</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/22uv1.png">
                  </span>
                  <span class="link">
                    <a href="http://www.cenchong.com/" target="_blank" rel="noopener">岑冲博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/ev9kl.png">
                  </span>
                  <span class="link">
                    <a href="http://www.zxiaoji.com/" target="_blank" rel="noopener">张小鸡</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://www.503error.com/favicon.ico">
                  </span>
                  <span class="link">
                    <a href="https://www.503error.com/" target="_blank" rel="noopener">张志明个人博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/x714o.jpg">
                  </span>
                  <span class="link">
                    <a href="http://www.hubwiz.com/" target="_blank" rel="noopener">汇智网</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/129d8.png">
                  </span>
                  <span class="link">
                    <a href="https://www.bysocket.com/" target="_blank" rel="noopener">泥瓦匠BYSocket</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://www.xiongge.club/favicon.ico">
                  </span>
                  <span class="link">
                    <a href="https://www.xiongge.club/" target="_blank" rel="noopener">熊哥club</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/3w4fe.png">
                  </span>
                  <span class="link">
                    <a href="https://zerlong.com/" target="_blank" rel="noopener">知语</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/44hxf.png">
                  </span>
                  <span class="link">
                    <a href="http://redstonewill.com/" target="_blank" rel="noopener">红色石头</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/8g1fk.jpg">
                  </span>
                  <span class="link">
                    <a href="http://www.laodong.me/" target="_blank" rel="noopener">老董博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/wkaus.jpg">
                  </span>
                  <span class="link">
                    <a href="https://zhaoshuai.me/" target="_blank" rel="noopener">碎念</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/pgo0r.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.chenwenguan.com/" target="_blank" rel="noopener">陈文管的博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/kk82a.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.lxlinux.net/" target="_blank" rel="noopener">良许Linux教程网</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/lj0t2.jpg">
                  </span>
                  <span class="link">
                    <a href="https://tanqingbo.cn/" target="_blank" rel="noopener">IT码农</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/i8cdr.png">
                  </span>
                  <span class="link">
                    <a href="https://junyiseo.com/" target="_blank" rel="noopener">均益个人博客</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/chwv2.png">
                  </span>
                  <span class="link">
                    <a href="https://brucedone.com/" target="_blank" rel="noopener">大鱼的鱼塘</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/2y43o.png">
                  </span>
                  <span class="link">
                    <a href="http://bbs.nightteam.cn/" target="_blank" rel="noopener">夜幕爬虫安全论坛</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/zvc3w.jpg">
                  </span>
                  <span class="link">
                    <a href="https://www.weishidong.com/" target="_blank" rel="noopener">韦世东的技术专栏</a>
                  </span>
                </li>
                <li class="friend">
                  <span class="logo">
                    <img src="https://qiniu.cuiqingcai.com/ebudy.jpg">
                  </span>
                  <span class="link">
                    <a href="https://chuanjiabing.com/" target="_blank" rel="noopener">穿甲兵技术社区</a>
                  </span>
                </li>
              </ul>
            </div>
          </div>
        </aside>
        <div id="sidebar-dimmer"></div>
      </div>
    </main>
    <footer class="footer">
      <div class="footer-inner">
        <div class="copyright"> &copy; <span itemprop="copyrightYear">2021</span>
          <span class="with-love">
            <i class="fa fa-heart"></i>
          </span>
          <span class="author" itemprop="copyrightHolder">崔庆才丨静觅</span>
          <span class="post-meta-divider">|</span>
          <span class="post-meta-item-icon">
            <i class="fa fa-chart-area"></i>
          </span>
          <span title="站点总字数">2.6m</span>
          <span class="post-meta-divider">|</span>
          <span class="post-meta-item-icon">
            <i class="fa fa-coffee"></i>
          </span>
          <span title="站点阅读时长">39:54</span>
        </div>
        <div class="powered-by">由 <a href="https://hexo.io/" class="theme-link" rel="noopener" target="_blank">Hexo</a> & <a href="https://pisces.theme-next.org/" class="theme-link" rel="noopener" target="_blank">NexT.Pisces</a> 强力驱动 </div>
        <div class="beian"><a href="https://beian.miit.gov.cn/" rel="noopener" target="_blank">京ICP备18015597号-1 </a>
        </div>
        <script>
          (function ()
          {
            function leancloudSelector(url)
            {
              url = encodeURI(url);
              return document.getElementById(url).querySelector('.leancloud-visitors-count');
            }

            function addCount(Counter)
            {
              var visitors = document.querySelector('.leancloud_visitors');
              var url = decodeURI(visitors.id);
              var title = visitors.dataset.flagTitle;
              Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify(
              {
                url
              }))).then(response => response.json()).then((
              {
                results
              }) =>
              {
                if (results.length > 0)
                {
                  var counter = results[0];
                  leancloudSelector(url).innerText = counter.time + 1;
                  Counter('put', '/classes/Counter/' + counter.objectId,
                  {
                    time:
                    {
                      '__op': 'Increment',
                      'amount': 1
                    }
                  }).catch(error =>
                  {
                    console.error('Failed to save visitor count', error);
                  });
                }
                else
                {
                  Counter('post', '/classes/Counter',
                  {
                    title,
                    url,
                    time: 1
                  }).then(response => response.json()).then(() =>
                  {
                    leancloudSelector(url).innerText = 1;
                  }).catch(error =>
                  {
                    console.error('Failed to create', error);
                  });
                }
              }).catch(error =>
              {
                console.error('LeanCloud Counter Error', error);
              });
            }

            function showTime(Counter)
            {
              var visitors = document.querySelectorAll('.leancloud_visitors');
              var entries = [...visitors].map(element =>
              {
                return decodeURI(element.id);
              });
              Counter('get', '/classes/Counter?where=' + encodeURIComponent(JSON.stringify(
              {
                url:
                {
                  '$in': entries
                }
              }))).then(response => response.json()).then((
              {
                results
              }) =>
              {
                for (let url of entries)
                {
                  let target = results.find(item => item.url === url);
                  leancloudSelector(url).innerText = target ? target.time : 0;
                }
              }).catch(error =>
              {
                console.error('LeanCloud Counter Error', error);
              });
            }
            let
            {
              app_id,
              app_key,
              server_url
            } = {
              "enable": true,
              "app_id": "6X5dRQ0pnPWJgYy8SXOg0uID-gzGzoHsz",
              "app_key": "ziLDVEy73ne5HtFTiGstzHMS",
              "server_url": "https://6x5drq0p.lc-cn-n1-shared.com",
              "security": false
            };

            function fetchData(api_server)
            {
              var Counter = (method, url, data) =>
              {
                return fetch(`${api_server}/1.1${url}`,
                {
                  method,
                  headers:
                  {
                    'X-LC-Id': app_id,
                    'X-LC-Key': app_key,
                    'Content-Type': 'application/json',
                  },
                  body: JSON.stringify(data)
                });
              };
              if (CONFIG.page.isPost)
              {
                if (CONFIG.hostname !== location.hostname) return;
                addCount(Counter);
              }
              else if (document.querySelectorAll('.post-title-link').length >= 1)
              {
                showTime(Counter);
              }
            }
            let api_server = app_id.slice(-9) !== '-MdYXbMMI' ? server_url : `https://${app_id.slice(0, 8).toLowerCase()}.api.lncldglobal.com`;
            if (api_server)
            {
              fetchData(api_server);
            }
            else
            {
              fetch('https://app-router.leancloud.cn/2/route?appId=' + app_id).then(response => response.json()).then((
              {
                api_server
              }) =>
              {
                fetchData('https://' + api_server);
              });
            }
          })();

        </script>
      </div>
      <div class="footer-stat">
        <span id="cnzz_stat_icon_1279355174"></span>
        <script type="text/javascript">
          document.write(unescape("%3Cspan id='cnzz_stat_icon_1279355174'%3E%3C/span%3E%3Cscript src='https://v1.cnzz.com/z_stat.php%3Fid%3D1279355174%26online%3D1%26show%3Dline' type='text/javascript'%3E%3C/script%3E"));

        </script>
      </div>
    </footer>
  </div>
  <script src="//cdn.jsdelivr.net/npm/animejs@3.2.1/lib/anime.min.js"></script>
  <script src="//cdn.jsdelivr.net/npm/pangu@4/dist/browser/pangu.min.js"></script>
  <script src="/js/utils.js"></script>
  <script src="/.js"></script>
  <script src="/js/schemes/pisces.js"></script>
  <script src="/.js"></script>
  <script src="/js/next-boot.js"></script>
  <script src="/.js"></script>
  <script>
    (function ()
    {
      var canonicalURL, curProtocol;
      //Get the <link> tag
      var x = document.getElementsByTagName("link");
      //Find the last canonical URL
      if (x.length > 0)
      {
        for (i = 0; i < x.length; i++)
        {
          if (x[i].rel.toLowerCase() == 'canonical' && x[i].href)
          {
            canonicalURL = x[i].href;
          }
        }
      }
      //Get protocol
      if (!canonicalURL)
      {
        curProtocol = window.location.protocol.split(':')[0];
      }
      else
      {
        curProtocol = canonicalURL.split(':')[0];
      }
      //Get current URL if the canonical URL does not exist
      if (!canonicalURL) canonicalURL = window.location.href;
      //Assign script content. Replace current URL with the canonical URL
      ! function ()
      {
        var e = /([http|https]:\/\/[a-zA-Z0-9\_\.]+\.baidu\.com)/gi,
          r = canonicalURL,
          t = document.referrer;
        if (!e.test(r))
        {
          var n = (String(curProtocol).toLowerCase() === 'https') ? "https://sp0.baidu.com/9_Q4simg2RQJ8t7jm9iCKT-xh_/s.gif" : "//api.share.baidu.com/s.gif";
          t ? (n += "?r=" + encodeURIComponent(document.referrer), r && (n += "&l=" + r)) : r && (n += "?l=" + r);
          var i = new Image;
          i.src = n
        }
      }(window);
    })();

  </script>
  <script src="/js/local-search.js"></script>
  <script src="/.js"></script>
</body>

</html>
